From 2894be86892eab4292a04db4fdf119512062c4b2 Mon Sep 17 00:00:00 2001 From: vklimontovich Date: Tue, 27 Apr 2021 17:19:12 +0300 Subject: [PATCH 1/6] Added telemetry collection (through jitsu.com) - Introduced useTelemetry() hook - Telemetry events are sent for page_view, booking_confirmed, time_selected, date_selected events - Telemetry is configured (and can be disabled) with NEXT_PUBLIC_TELEMETRY_KEY env variable --- .env.example | 5 +++- components/Shell.tsx | 10 ++++++- lib/telemetry.ts | 62 +++++++++++++++++++++++++++++++++++++++++ package.json | 1 + pages/[user]/[type].tsx | 7 ++++- pages/[user]/book.tsx | 7 +++++ pages/_app.tsx | 9 ++++-- yarn.lock | 5 ++++ 8 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 lib/telemetry.ts diff --git a/.env.example b/.env.example index 269c048b..9fa0bef4 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,10 @@ -DATABASE_URL='postgresql://:@:' +DATABASE_URL='postgresql://:@:/' GOOGLE_API_CREDENTIALS='secret' NEXTAUTH_URL='http://localhost:3000' +# Remove this var if you don't want Calendso to collect anonymous usage +NEXT_PUBLIC_TELEMETRY_KEY=js.2pvs2bbpqq1zxna97wcml.oi2jzirnbj1ev4tc57c5r + # Used for the Office 365 / Outlook.com Calendar integration MS_GRAPH_CLIENT_ID= MS_GRAPH_CLIENT_SECRET= \ No newline at end of file diff --git a/components/Shell.tsx b/components/Shell.tsx index bafbd386..e0a8fe0f 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -1,14 +1,22 @@ import Link from 'next/link'; -import { useState } from "react"; +import {useContext, useEffect, useState} from "react"; import { useRouter } from "next/router"; import { signOut, useSession } from 'next-auth/client'; import { MenuIcon, XIcon } from '@heroicons/react/outline'; +import {TelemetryContext, useTelemetry} from "../lib/telemetry"; export default function Shell(props) { const router = useRouter(); const [ session, loading ] = useSession(); const [ profileDropdownExpanded, setProfileDropdownExpanded ] = useState(false); const [ mobileMenuExpanded, setMobileMenuExpanded ] = useState(false); + let telemetry = useTelemetry(); + + useEffect(() => { + telemetry.withJitsu((jitsu) => { + return jitsu.track('page_view', {page_url: router.pathname}) + }); + }, [telemetry]) const toggleProfileDropdown = () => { setProfileDropdownExpanded(!profileDropdownExpanded); diff --git a/lib/telemetry.ts b/lib/telemetry.ts new file mode 100644 index 00000000..5cc38163 --- /dev/null +++ b/lib/telemetry.ts @@ -0,0 +1,62 @@ +import React, {useContext} from 'react' +import {jitsuClient, JitsuClient} from "@jitsu/sdk-js"; + + +/** + * 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 +} + +const emptyClient: TelemetryClient = {withJitsu: () => {}}; + +function useTelemetry(): TelemetryClient { + return useContext(TelemetryContext); +} + +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: "js.2pvs2bbpqq1zxna97wcml.oi2jzirnbj1ev4tc57c5r", + cookie_name: "__clnds", + capture_3rd_party_cookies: false, + }); + } + let res = callback(window['jitsu']); + if (res && typeof res['catch'] === "function") { + res.catch(e => { + console.debug("Unable to send telemetry event", e) + }); + } + } + } + } else { + return emptyClient; + } +} + + +const TelemetryContext = React.createContext(emptyClient) + +const TelemetryProvider = TelemetryContext.Provider + +export { TelemetryContext, TelemetryProvider, createTelemetryClient, useTelemetry }; diff --git a/package.json b/package.json index cc4b0df1..d5e522b4 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@headlessui/react": "^1.0.0", "@heroicons/react": "^1.0.1", + "@jitsu/sdk-js": "^2.0.0", "@prisma/client": "2.21.2", "@tailwindcss/forms": "^0.2.1", "bcryptjs": "^2.4.3", diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 1b9392e7..a49ba804 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -16,6 +16,7 @@ dayjs.extend(utc); dayjs.extend(timezone); import getSlots from '../../lib/slots'; +import {useTelemetry} from "../../lib/telemetry"; function classNames(...classes) { return classes.filter(Boolean).join(' ') @@ -29,6 +30,7 @@ export default function Type(props) { const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); const [is24h, setIs24h] = useState(false); const [busy, setBusy] = useState([]); + const telemetry = useTelemetry(); // Get router variables const router = useRouter(); @@ -68,7 +70,10 @@ export default function Type(props) { } const calendar = days.map((day) => - ); diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index 537c0437..8fcc835c 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -3,14 +3,21 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; import { ClockIcon, CalendarIcon } from '@heroicons/react/solid'; import prisma from '../../lib/prisma'; +import {useTelemetry} from "../../lib/telemetry"; +import {useEffect} from "react"; const dayjs = require('dayjs'); export default function Book(props) { const router = useRouter(); const { date, user } = router.query; + const telemetry = useTelemetry(); + useEffect(() => { + telemetry.withJitsu(jitsu => jitsu.track('time_selected')); + }) const bookingHandler = event => { event.preventDefault(); + telemetry.withJitsu(jitsu => jitsu.track('booking_confirmed')); const res = fetch( '/api/book/' + user, { diff --git a/pages/_app.tsx b/pages/_app.tsx index 2bbf669d..e2964dde 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,11 +1,14 @@ import '../styles/globals.css'; +import {createTelemetryClient, TelemetryProvider} from '../lib/telemetry'; import { Provider } from 'next-auth/client'; function MyApp({ Component, pageProps }) { return ( - - - + + + + + ); } diff --git a/yarn.lock b/yarn.lock index 6921863c..6cbce375 100644 --- a/yarn.lock +++ b/yarn.lock @@ -160,6 +160,11 @@ resolved "https://registry.npmjs.org/@heroicons/react/-/react-1.0.1.tgz" integrity sha512-uikw2gKCmqnvjVxitecWfFLMOKyL9BTFcU4VM3hHj9OMwpkCr5Ke+MRMyY2/aQVmsYs4VTq7NCFX05MYwAHi3g== +"@jitsu/sdk-js@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@jitsu/sdk-js/-/sdk-js-2.0.0.tgz#01ef96c602b3b2aa1e1a4bf87e868f7a5bfe3b35" + integrity sha512-+IQLEbzrIpuXKmP2bLbD7eAdF1WaEcZ2eaSMl6AAwcE0BEFctjNG8QhQYWsMgb+KahNKFz1ARll3aJegkqgrew== + "@next/env@10.0.8": version "10.0.8" resolved "https://registry.npmjs.org/@next/env/-/env-10.0.8.tgz" From 69f34976585ec368f1a2754ffc74b291611c97aa Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Mon, 3 May 2021 16:59:49 +0000 Subject: [PATCH 2/6] Fixes #134, invalid add to google calendar link on success page. --- pages/success.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/success.tsx b/pages/success.tsx index 78c7431a..fd1c2407 100644 --- a/pages/success.tsx +++ b/pages/success.tsx @@ -74,7 +74,7 @@ export default function Success(props) {
Add to your calendar
- Google From b8d570c8db9715620864040ad941dbc19eeaa1a3 Mon Sep 17 00:00:00 2001 From: Ryan Jung Date: Mon, 3 May 2021 16:33:07 -0400 Subject: [PATCH 3/6] changed input type to email --- pages/[user]/book.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index 2ec3e924..462eae64 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -65,7 +65,7 @@ export default function Book(props) {
- +
From acf29cdaa5b83c78fd483534246aeec8a5813437 Mon Sep 17 00:00:00 2001 From: Ryan Jung Date: Mon, 3 May 2021 16:56:33 -0400 Subject: [PATCH 4/6] make name and email fields required --- pages/[user]/book.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index 462eae64..542208e2 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -59,13 +59,13 @@ export default function Book(props) {
- +
- +
From df88919d159757539864829f55156714af4ab913 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Tue, 4 May 2021 11:36:06 +0000 Subject: [PATCH 5/6] Remove the 'Cancel' button from the Settings pages suggested in #140 --- pages/settings/password.tsx | 3 --- pages/settings/profile.tsx | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pages/settings/password.tsx b/pages/settings/password.tsx index 8644d3c7..df8ec35a 100644 --- a/pages/settings/password.tsx +++ b/pages/settings/password.tsx @@ -74,9 +74,6 @@ export default function Settings(props) {

- diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index e536f8b1..17b4624c 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -145,9 +145,6 @@ export default function Settings(props) {

- @@ -181,4 +178,4 @@ export async function getServerSideProps(context) { return { props: {user}, // will be passed to the page component as props } -} +} \ No newline at end of file From 4969258e62b43e9ebc0f0d4af0e0393206148b4e Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Tue, 4 May 2021 20:31:15 +0000 Subject: [PATCH 6/6] Take server config into account whilst listing integrations, improved feedback for installer as per #142 --- next.config.js | 15 ++++ pages/integrations/index.tsx | 160 ++++++++++++++++++----------------- 2 files changed, 98 insertions(+), 77 deletions(-) diff --git a/next.config.js b/next.config.js index 414aee90..9ffb8597 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,20 @@ const withTM = require('next-transpile-modules')(['react-timezone-select']); +const validJson = (jsonString) => { + try { + const o = JSON.parse(jsonString); + if (o && typeof o === "object") { + return o; + } + } + catch (e) {} + return false; +} + +if (process.env.GOOGLE_API_CREDENTIALS && ! validJson(process.env.GOOGLE_API_CREDENTIALS)) { + console.warn('\x1b[33mwarn', '\x1b[0m', "- Disabled 'Google Calendar' integration. Reason: Invalid value for GOOGLE_API_CREDENTIALS environment variable. When set, this value needs to contain valid JSON like {\"web\":{\"client_id\":\"\",\"client_secret\":\"\",\"redirect_uris\":[\"/api/integrations/googlecalendar/callback>\"]}. You can download this JSON from your OAuth Client @ https://console.cloud.google.com/apis/credentials."); +} + module.exports = withTM({ future: { webpack5: true, diff --git a/pages/integrations/index.tsx b/pages/integrations/index.tsx index edb01a29..76e16a34 100644 --- a/pages/integrations/index.tsx +++ b/pages/integrations/index.tsx @@ -7,7 +7,7 @@ import { useSession, getSession } from 'next-auth/client'; import { CheckCircleIcon, XCircleIcon, ChevronRightIcon, PlusIcon } from '@heroicons/react/solid'; import { InformationCircleIcon } from '@heroicons/react/outline'; -export default function Home(props) { +export default function Home({ integrations }) { const [session, loading] = useSession(); const [showAddModal, setShowAddModal] = useState(false); @@ -24,7 +24,7 @@ export default function Home(props) { } function integrationHandler(type) { - fetch('/api/integrations/' + type + '/add') + fetch('/api/integrations/' + type.replace('_', '') + '/add') .then((response) => response.json()) .then((data) => window.location.href = data.url); } @@ -38,73 +38,63 @@ export default function Home(props) {
- - {props.credentials.length == 0 && -
-
-
- + : +
+
+
+ +
+
+

+ You don't have any integrations added. +

+
+

+ You currently do not have any integrations set up. Add your first integration to get started. +

-
-

- You don't have any integrations added. -

-
-

- You currently do not have any integrations set up. Add your first integration to get started. -

-
-
- -
+
+
+
}
{showAddModal && @@ -150,30 +140,18 @@ export default function Home(props) {
    -
  • + {integrations.filter( (integration) => integration.installed ).map( (integration) => (
  • - Office 365 / Outlook.com Calendar + {integration.title}
    -

    Office 365 / Outlook.com Calendar

    -

    For personal and business accounts

    +

    { integration.title }

    +

    { integration.description }

    - +
    -
  • -
  • -
    - Google Calendar -
    -
    -

    Google Calendar

    -

    For personal and business accounts

    -
    -
    - -
    -
  • + ))}
@@ -190,6 +168,17 @@ export default function Home(props) { ); } +const validJson = (jsonString: string) => { + try { + const o = JSON.parse(jsonString); + if (o && typeof o === "object") { + return o; + } + } + catch (e) {} + return false; +} + export async function getServerSideProps(context) { const session = await getSession(context); @@ -212,7 +201,24 @@ export async function getServerSideProps(context) { key: true } }); + + const integrations = [ { + installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)), + credential: credentials.find( (integration) => integration.type === "google_calendar" ) || null, + type: "google_calendar", + title: "Google Calendar", + imageSrc: "integrations/google-calendar.png", + description: "For personal and business accounts", + }, { + installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET), + type: "office365_calendar", + credential: credentials.find( (integration) => integration.type === "office365_calendar" ) || null, + title: "Office 365 / Outlook.com Calendar", + imageSrc: "integrations/office-365.png", + description: "For personal and business accounts", + } ]; + return { - props: {credentials}, // will be passed to the page component as props + props: {integrations}, } -} +} \ No newline at end of file