diff --git a/package-lock.json b/package-lock.json index baccd3b0..d4f1be67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "packages/*" ], "dependencies": { + "@segment/analytics-next": "^1.76.0", "dompurify": "^3.2.0", + "posthog-js": "^1.194.4", "react-dropzone": "^14.2.3" }, "devDependencies": { @@ -3241,6 +3243,27 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@lukeed/uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lukeed/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@mdx-js/util": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.16.tgz", @@ -3973,6 +3996,87 @@ "react-dom": "15.x || 16.x || 17.x || 18.x" } }, + "node_modules/@segment/analytics-core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@segment/analytics-core/-/analytics-core-1.8.0.tgz", + "integrity": "sha512-6CrccsYRY33I3mONN2ZW8SdBpbLtu1Ict3xR+n0FemYF5RB/jG7pW6jOvDXULR8kuYMzMmGOP4HvlyUmf3qLpg==", + "license": "MIT", + "dependencies": { + "@lukeed/uuid": "^2.0.0", + "@segment/analytics-generic-utils": "1.2.0", + "dset": "^3.1.4", + "tslib": "^2.4.1" + } + }, + "node_modules/@segment/analytics-generic-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@segment/analytics-generic-utils/-/analytics-generic-utils-1.2.0.tgz", + "integrity": "sha512-DfnW6mW3YQOLlDQQdR89k4EqfHb0g/3XvBXkovH1FstUN93eL1kfW9CsDcVQyH3bAC5ZsFyjA/o/1Q2j0QeoWw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.1" + } + }, + "node_modules/@segment/analytics-next": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@segment/analytics-next/-/analytics-next-1.76.0.tgz", + "integrity": "sha512-4n4vMvX0+bfypFWuu/UJNenT/Gv2+04SsjvnQL1eBd1hngKBV56EkCW+PCJyFRQQ7BnzHgWF4mY+vOPkdoke3A==", + "license": "MIT", + "dependencies": { + "@lukeed/uuid": "^2.0.0", + "@segment/analytics-core": "1.8.0", + "@segment/analytics-generic-utils": "1.2.0", + "@segment/analytics.js-video-plugins": "^0.2.1", + "@segment/facade": "^3.4.9", + "dset": "^3.1.4", + "js-cookie": "3.0.1", + "node-fetch": "^2.6.7", + "tslib": "^2.4.1", + "unfetch": "^4.1.0" + } + }, + "node_modules/@segment/analytics.js-video-plugins": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@segment/analytics.js-video-plugins/-/analytics.js-video-plugins-0.2.1.tgz", + "integrity": "sha512-lZwCyEXT4aaHBLNK433okEKdxGAuyrVmop4BpQqQSJuRz0DglPZgd9B/XjiiWs1UyOankg2aNYMN3VcS8t4eSQ==", + "license": "ISC", + "dependencies": { + "unfetch": "^3.1.1" + } + }, + "node_modules/@segment/analytics.js-video-plugins/node_modules/unfetch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-3.1.2.tgz", + "integrity": "sha512-L0qrK7ZeAudGiKYw6nzFjnJ2D5WHblUBwmHIqtPS6oKUd+Hcpk7/hKsSmcHsTlpd1TbTNsiRBUKRq3bHLNIqIw==", + "license": "MIT" + }, + "node_modules/@segment/facade": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/@segment/facade/-/facade-3.4.10.tgz", + "integrity": "sha512-xVQBbB/lNvk/u8+ey0kC/+g8pT3l0gCT8O2y9Z+StMMn3KAFAQ9w8xfgef67tJybktOKKU7pQGRPolRM1i1pdA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@segment/isodate-traverse": "^1.1.1", + "inherits": "^2.0.4", + "new-date": "^1.0.3", + "obj-case": "0.2.1" + } + }, + "node_modules/@segment/isodate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@segment/isodate/-/isodate-1.0.3.tgz", + "integrity": "sha512-BtanDuvJqnACFkeeYje7pWULVv8RgZaqKHWwGFnL/g/TH/CcZjkIVTfGDp/MAxmilYHUkrX70SqwnYSTNEaN7A==", + "license": "SEE LICENSE IN LICENSE" + }, + "node_modules/@segment/isodate-traverse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@segment/isodate-traverse/-/isodate-traverse-1.1.1.tgz", + "integrity": "sha512-+G6e1SgAUkcq0EDMi+SRLfT48TNlLPF3QnSgFGVs0V9F3o3fq/woQ2rHFlW20W0yy5NnCUH0QGU3Am2rZy/E3w==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@segment/isodate": "^1.0.3" + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -10107,6 +10211,15 @@ "node": ">=4" } }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -10253,7 +10366,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, + "devOptional": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -10262,7 +10375,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -11904,6 +12017,12 @@ "pend": "~1.2.0" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -13811,8 +13930,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -16867,6 +16985,15 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/js-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", + "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -19537,6 +19664,15 @@ "integrity": "sha512-ye8AIYWQcP9MvoM1i0Z2jV0qed31Z8EWXYnyGNkiUAd+Fo8J+7uy90xTV8g/oAbhtjkY7iZbNTizQaXdKUuwpQ==", "dev": true }, + "node_modules/new-date": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/new-date/-/new-date-1.0.3.tgz", + "integrity": "sha512-0fsVvQPbo2I18DT2zVHpezmeeNYV2JaJSrseiHLc17GNOxJzUdx5mvSigPu8LtIfZSij5i1wXnXFspEs2CD6hA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@segment/isodate": "1.0.3" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -19593,7 +19729,6 @@ "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -19627,20 +19762,17 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -19832,6 +19964,12 @@ "node": "*" } }, + "node_modules/obj-case": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/obj-case/-/obj-case-0.2.1.tgz", + "integrity": "sha512-PquYBBTy+Y6Ob/O2574XHhDtHJlV1cJHMCgW+rDRc9J5hhmRelJB3k5dTK/3cVmFVtzvAKuENeuLpoyTzMzkOg==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -20846,6 +20984,39 @@ "dev": true, "license": "ISC" }, + "node_modules/posthog-js": { + "version": "1.194.4", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.194.4.tgz", + "integrity": "sha512-w42HfzQktNj51jb4lPf45qykPG0RSpl2WZmr1kAaPTVF9LKUwRJHA854GN0R0bx1hap2fnao/yQBktFLlg4vxQ==", + "license": "MIT", + "dependencies": { + "core-js": "^3.38.1", + "fflate": "^0.4.8", + "preact": "^10.19.3", + "web-vitals": "^4.2.0" + } + }, + "node_modules/posthog-js/node_modules/core-js": { + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/preact": { + "version": "10.25.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.1.tgz", + "integrity": "sha512-frxeZV2vhQSohQwJ7FvlqC40ze89+8friponWUFeVEkaCfhC6Eu4V0iND5C9CXz8JLndV07QRDeXzH1+Anz5Og==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -23204,7 +23375,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "node_modules/sass": { "version": "1.72.0", @@ -25807,6 +25978,12 @@ "through": "^2.3.8" } }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", + "license": "MIT" + }, "node_modules/unherit": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", @@ -26848,6 +27025,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index b07db0e5..673bbad5 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,9 @@ }, "dependencies": { "dompurify": "^3.2.0", - "react-dropzone": "^14.2.3" + "react-dropzone": "^14.2.3", + "@segment/analytics-next": "^1.76.0", + "posthog-js": "^1.194.4" }, "packageManager": "yarn@4.5.0+sha512.837566d24eec14ec0f5f1411adb544e892b3454255e61fdef8fd05f3429480102806bac7446bc9daff3896b01ae4b62d00096c7e989f1596f2af10b927532f39" } diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/about-chatbot.md b/packages/module/patternfly-docs/content/extensions/chatbot/about-chatbot.md index 6595af30..c15e5f3a 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/about-chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/about-chatbot.md @@ -34,5 +34,8 @@ Explore our documentation, which covers both ChatBot UI components and message-r - Messages - [Bot and user messages](/patternfly-ai/chatbot/messages) - [File attachments](/patternfly-ai/chatbot/messages#attachments) +- Analytics support + - [Overview](/patternfly-ai/chatbot/analytics) + We will continue to grow and evolve our ChatBot. If you notice a bug or have any suggestions, feel free to file an issue in our [GitHub repository!](https://github.com/patternfly/chatbot/issues) Make sure to check if there is already a pre-existing issue before creating a new one. diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md new file mode 100644 index 00000000..5204d401 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md @@ -0,0 +1,210 @@ +--- +# Sidenav top-level section +# should be the same for all markdown files +section: PatternFly-AI +subsection: ChatBot +# Sidenav secondary level section +# should be the same for all markdown files +id: Analytics +# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility) +source: react +# If you use typescript, the name of the interface to display props for +# These are found through the sourceProps function provided in patternfly-docs.source.js +propComponents: + [ + + ] +sortValue: 4 +--- + +## Overview + +The following diagram shows the main components of the Analytics tracking code: + +
+![Main elements of the tracking code.](./analytics-layout.svg) +
+ +The user code first calls the static `getTrackingProviders()` method which initialises the tracking providers. +This returns an instance of the `TrackingAPI` , which can then subsequently be used to emit analytics events. + +User code (blueish box) only interacts with the classes/methods in green. + +## Usage + +Before the code can be used, it is necessary to once supply the api-keys of the respective providers: +```nolive +const initProps: InitProps = { + segmentKey: 'TODO-key', // TODO add your key here +// segmentCdn: 'https://my.org/cdn', // Set up segment cdn (optional) +// segmentIntegrations: { // Provide Segment integations (optional) +// 'Segment.io': { +// apiHost: 'my.api.host/api/v1', +// protocol: 'https' +// } + }, + + posthogKey: 'TODO-key', + umamiKey: 'TODO-key', + umamiHostUrl: 'http://localhost:3000', // TODO where is your JS provider? + something: 'test', + console: 'true' // Console provider +}; +``` + +Once this is done, you can get an instance of the `TrackingAPI` and then start sending events. +One of the first events should be to identify the user in some way (this can be a UUID, which stays the same for the same user). + + + +```nolive +const trackingAPI = getTrackingProviders(initProps); +trackingAPI.identify('user-123'); // TODO get real user id +// Track the page, that is currently visited. Best put into a react effect (see below) +trackingAPI.trackPageView(); +// Track a single Event +trackingAPI.trackSingleItem("MyEvent", { response: 'Good response' }) +``` + +### Page hit tracking + +For page flow tracking, you can use a snippet like this. + +```nolive +import React from 'react'; +import { useLocation } from 'react-router-dom'; + +export const useTrackPageFlow = (): void => { + const { pathname } = useLocation(); + + // notify url change events + React.useEffect(() => { + trackingAPI.trackPageView(); + }, [pathname]); +}; +``` + +### Single Event tracking + +To track single events (e.g. Button Press, Form Submission), you use the `trackSingleItem` method. + +```nolive +trackingAPI.trackSingleItem(eventName, propertyDict) +``` + +The method takes two parameters: + +* eventName : name of the event. Should be unique throughout the application (or you need to differentiate different events with the same name by supplying an additional property). +* propertyDict: a dict with key-value pairs that represent important properties of the event. The dict can be empty. + +When submitting forms, you only instrument the form itself. Don't instrument the button that calls the form. +Two major cases need to be distinguished: + +* Form submitted +* * Form submission action was successful +* * Form submission action failed +* Form was cancelled + +For form submission, you can use the +```nolive +trackingAPI.trackSingleItem(Event_Name, { + outcome: << submit , cancel >>, + success? : boolean, + error? : string, + , string/number/boolean } ) +``` + +call. Outcome is obvious, for `outcome=submit`, `success` denotes if the action (in the backend) was successful or not. +For the case of failure, `error` would yield the error message (try to strip out variable parts like the random part of a container-name). +`` are some additional properties from the form, that should be sent to analytics. +Take good judgement, what to send. +Items like names provided by the user above should not be sent. +Likewise for the content of a description field. +On the other hand for Deployments, replica count or memory server size are items that could be sent and will help us to better understand the users. + + +### Enable analytics gathering + +Only providers that have a matching key in the `InitProps` will be started and used. + +```nolive +const initProps: InitProps = { + segmentKey: 'TODO-key', // TODO add your key here + posthogKey: 'TODO-key', + umamiKey: 'TODO-key', + umamiHostUrl: 'http://localhost:3000', // TODO where is your JS provider? + console: true +``` + +If you know upfront that you only want to use one of the providers, it is possible to modify +`getTrackingProviders()` and omit the unneeded providers in the providers array. + +### package.json + +When using the providers you need to add some dependencies in package.json like: + +```nolive +"dependencies": { + "@segment/analytics-next": "^1.76.0", + "posthog-js": "^1.194.4" +``` + +## Examples + +I have started the ChatBot and done the actions 1-5 in order: + +
+![Events done in the chatbot.](./Events.png) +
+ +1. Select a Model +2. Posted a question +3. Got an answer from the model +4. Clicked the thumbsUp button +5. Closed the chatbot + +### Segment + +Segment shows the events in its Source debugger with the newest event at the top. +Below the numbered events, you can also see the results of `identify` and `trackPageView`. + +
+![Events display in the Segment debugger.](./Events_segment.png) +
+ +If you clicked on an event, you'd get to see the passed properties. + +**Note**: When using the Segment provider, you may also want to set the +`segmentCdn` and `segmentIntegrations` initialization properties. + +### Umami + +Events are visible in Umami under Website -> Events: + +The list is pretty similar to Segment, just differently formatted. + +
+![Events in Umami.](./Events_umami.png) +
+ +### PostHog + +PostHog shows the Events in the Activity section. + +
+![Events in PostHog.](./Events_posthog.png) +
+ +PostHog sends more events, as it integrates deeper in the provided code, +you can nevertheless see the main events that we have created in our code. + + +## Add a new analytics provider + +To add another analytics provider, you need to implement two interfaces, `TrackingSpi` and `TrackingApi`. +Most easy is probably to copy the `ConsoleTrackingProvider`. +The first thing you should do is to provide a correct value in `getKey()`. +Once you are happy enough with the implementation, you should add it in `getTrackingProviders()` to the array of providers. + + + diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events.png b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events.png new file mode 100644 index 00000000..41a7a062 Binary files /dev/null and b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events.png differ diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events_Segment.png b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events_Segment.png new file mode 100644 index 00000000..a220057b Binary files /dev/null and b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events_Segment.png differ diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events_posthog.png b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events_posthog.png new file mode 100644 index 00000000..dd07a7a6 Binary files /dev/null and b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events_posthog.png differ diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events_umami.png b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events_umami.png new file mode 100644 index 00000000..ddb8fa01 Binary files /dev/null and b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Events_umami.png differ diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/analytics-layout.svg b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/analytics-layout.svg new file mode 100644 index 00000000..c49c7522 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/analytics-layout.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + Canvas 1 + + Layer 1 + + + + + User + Code + + + + + + + TrackingProviderProxy + + + + + + + + Provider + + + + + + + Provider + + + + + + + Provider + + + + + + + + <<interface>> + TrackingSPI + + + + + + + + + + + + + + + <<registers>> + + + + + + + + + <<creates> + + + + + + <<initializes>> + + + + + + + + TrackingRegistry + + + + + + + + getTrackingProviders + + + + + + + + + <<interface> + TrackingAPI + + + + + + + + identify + + trackPageView + +trackSingleItem + + + + + + + + + + + + + <<uses>> + + + + + diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md index e657d3a0..52b0a0e8 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md @@ -57,6 +57,8 @@ import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; import userAvatar from '../Messages/user_avatar.svg'; import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; +import { getTrackingProviders } from "@patternfly/chatbot/dist/dynamic/tracking"; + ### Basic ChatBot @@ -81,6 +83,7 @@ This demo displays a basic ChatBot, which includes: 7. A "Skip to chatbot" button that allows you to skip to the chatbot content via the [PatternFly skip to content component](/patternfly-ai/chatbot/ui#skip-to-content). To display this button you must tab into the main window. + ```js file="./Chatbot.tsx" isFullscreen ``` @@ -126,3 +129,7 @@ Your code structure should look like this: ```js file="./EmbeddedComparisonChatbot.tsx" isFullscreen ``` + + +### Analytics + diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.tsx index 7c43b73b..0ed8bc72 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.tsx @@ -32,6 +32,8 @@ import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; import userAvatar from '../Messages/user_avatar.svg'; import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; +import { getTrackingProviders } from '@patternfly/chatbot/dist/dynamic/tracking'; +import { InitProps } from '@patternfly/chatbot/dist/dynamic/tracking/'; const footnoteProps = { label: 'ChatBot uses AI. Check for mistakes.', @@ -95,6 +97,20 @@ export default MessageLoading; // The timestamps re-render with them. const date = new Date(); +const initProps: InitProps = { + segmentKey: 'TODO-key', // TODO add your key here + posthogKey: 'TODO-key', + umamiKey: 'TODO-key', + umamiHostUrl: 'http://localhost:3000', // TODO where is your JS provider? + console: true, + something: 'test' +}; + +const tracking = getTrackingProviders(initProps); +tracking.identify('user-123'); // TODO get real user id +tracking.trackPageView(); + +const actionEventName = 'MessageAction'; const initialMessages: MessageProps[] = [ { id: '1', @@ -113,16 +129,11 @@ const initialMessages: MessageProps[] = [ avatar: patternflyAvatar, timestamp: date.toLocaleString(), actions: { - // eslint-disable-next-line no-console - positive: { onClick: () => console.log('Good response') }, - // eslint-disable-next-line no-console - negative: { onClick: () => console.log('Bad response') }, - // eslint-disable-next-line no-console - copy: { onClick: () => console.log('Copy') }, - // eslint-disable-next-line no-console - share: { onClick: () => console.log('Share') }, - // eslint-disable-next-line no-console - listen: { onClick: () => console.log('Listen') } + positive: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Good response' }) }, + negative: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Bad response' }) }, + copy: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Copy' }) }, + share: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Share' }) }, + listen: { onClick: () => tracking.trackSingleItem(actionEventName, { response: 'Listen' }) } } } ]; @@ -164,6 +175,7 @@ const initialConversations = { ] }; +const actionEvent2 = 'ActionEvent2'; export const ChatbotDemo: React.FunctionComponent = () => { const [chatbotVisible, setChatbotVisible] = React.useState(true); const [displayMode, setDisplayMode] = React.useState(ChatbotDisplayMode.default); @@ -180,7 +192,7 @@ export const ChatbotDemo: React.FunctionComponent = () => { const chatbotRef = React.useRef(null); const historyRef = React.useRef(null); - // Autu-scrolls to the latest message + // Auto-scrolls to the latest message React.useEffect(() => { // don't scroll the first load - in this demo, we know we start with two messages if (messages.length > 2) { @@ -193,6 +205,7 @@ export const ChatbotDemo: React.FunctionComponent = () => { value: string | number | undefined ) => { setSelectedModel(value as string); + tracking.trackSingleItem('ModelSelected', { model: value }); }; const onSelectDisplayMode = ( @@ -210,6 +223,7 @@ export const ChatbotDemo: React.FunctionComponent = () => { const handleSend = (message: string) => { setIsSendButtonDisabled(true); + tracking.trackSingleItem('UserInputReceived', { text: message }); const newMessages: MessageProps[] = []; // We can't use structuredClone since messages contains functions, but we can't mutate // items that are going into state or the UI won't update correctly @@ -255,21 +269,17 @@ export const ChatbotDemo: React.FunctionComponent = () => { avatar: patternflyAvatar, timestamp: date.toLocaleString(), actions: { - // eslint-disable-next-line no-console - positive: { onClick: () => console.log('Good response') }, - // eslint-disable-next-line no-console - negative: { onClick: () => console.log('Bad response') }, - // eslint-disable-next-line no-console - copy: { onClick: () => console.log('Copy') }, - // eslint-disable-next-line no-console - share: { onClick: () => console.log('Share') }, - // eslint-disable-next-line no-console - listen: { onClick: () => console.log('Listen') } + positive: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Good response' }) }, + negative: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Bad response' }) }, + copy: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Copy' }) }, + share: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Share' }) }, + listen: { onClick: () => tracking.trackSingleItem(actionEvent2, { response: 'Listen' }) } } }); setMessages(loadedMessages); // make announcement to assistive devices that new message has loaded setAnnouncement(`Message from Bot: API response goes here`); + tracking.trackSingleItem('BotResponded', { undefined }); setIsSendButtonDisabled(false); }, 5000); }; @@ -339,7 +349,10 @@ export const ChatbotDemo: React.FunctionComponent = () => { setChatbotVisible(!chatbotVisible)} + onToggleChatbot={function () { + setChatbotVisible(!chatbotVisible); + tracking.trackSingleItem('Chatbot Visible', { isVisible: !chatbotVisible }); // TODO correct? + }} id="chatbot-toggle" ref={toggleRef} /> @@ -441,9 +454,9 @@ export const ChatbotDemo: React.FunctionComponent = () => { prompts={welcomePrompts} /> {/* This code block enables scrolling to the top of the last message. - You can instead choose to move the div with scrollToBottomRef on it below + You can instead choose to move the div with scrollToBottomRef on it below the map of messages, so that users are forced to scroll to the bottom. - If you are using streaming, you will want to take a different approach; + If you are using streaming, you will want to take a different approach; see: https://github.com/patternfly/chatbot/issues/201#issuecomment-2400725173 */} {messages.map((message, index) => { if (index === messages.length - 1) { diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 6e1cc449..cc11b8d7 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -80,3 +80,6 @@ export * from './SourcesCard'; export { default as TermsOfUse } from './TermsOfUse'; export * from './TermsOfUse'; + +export { default as tracking } from './tracking'; +export * from './tracking'; diff --git a/packages/module/src/tracking/console_tracking_provider.ts b/packages/module/src/tracking/console_tracking_provider.ts new file mode 100644 index 00000000..178b25b7 --- /dev/null +++ b/packages/module/src/tracking/console_tracking_provider.ts @@ -0,0 +1,30 @@ +import { TrackingSpi } from './tracking_spi'; +import { TrackingApi, TrackingEventProperties } from './tracking_api'; + +export class ConsoleTrackingProvider implements TrackingSpi, TrackingApi { + trackPageView(url: string | undefined) { + // eslint-disable-next-line no-console + console.log('ConsoleProvider pageView', url); + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + registerProvider(): void {} + + initialize(): void { + // eslint-disable-next-line no-console + console.log('ConsoleProvider initialize'); + } + + identify(userID: string): void { + // eslint-disable-next-line no-console + console.log('ConsoleProvider identify', userID); + } + + trackSingleItem(item: string, properties?: TrackingEventProperties): void { + // eslint-disable-next-line no-console + console.log('ConsoleProvider: ' + item, properties); + } + + getKey(): string { + return 'console'; + } +} diff --git a/packages/module/src/tracking/index.ts b/packages/module/src/tracking/index.ts new file mode 100644 index 00000000..b50c1e43 --- /dev/null +++ b/packages/module/src/tracking/index.ts @@ -0,0 +1,3 @@ +export { default } from './tracking_registry'; + +export * from './tracking_registry'; diff --git a/packages/module/src/tracking/posthog_tracking_provider.ts b/packages/module/src/tracking/posthog_tracking_provider.ts new file mode 100644 index 00000000..fb8c7b93 --- /dev/null +++ b/packages/module/src/tracking/posthog_tracking_provider.ts @@ -0,0 +1,42 @@ +import { posthog } from 'posthog-js'; + +import { TrackingApi, TrackingEventProperties } from './tracking_api'; +import { InitProps, TrackingSpi } from './tracking_spi'; + +export class PosthogTrackingProvider implements TrackingSpi, TrackingApi { + getKey(): string { + return 'posthogKey'; + } + + initialize(props: InitProps): void { + // eslint-disable-next-line no-console + console.log('PosthogProvider initialize'); + const posthogKey = props.posthogKey as string; + + posthog.init(posthogKey, { + // eslint-disable-next-line camelcase + api_host: 'https://us.i.posthog.com', + // eslint-disable-next-line camelcase + person_profiles: 'identified_only' // or 'always' to create profiles for anonymous users as well + }); + } + + identify(userID: string): void { + // eslint-disable-next-line no-console + console.log('PosthogProvider userID: ' + userID); + posthog.identify(userID); + } + + trackPageView(url: string | undefined): void { + // eslint-disable-next-line no-console + console.log('PostHogProvider url', url); + // TODO posthog seems to record that automatically. + // How to not clash with this here? Just leave as no-op? + } + + trackSingleItem(item: string, properties?: TrackingEventProperties): void { + // eslint-disable-next-line no-console + console.log('PosthogProvider: trackSingleItem' + item, properties); + posthog.capture(item, { properties }); + } +} diff --git a/packages/module/src/tracking/segment_tracking_provider.ts b/packages/module/src/tracking/segment_tracking_provider.ts new file mode 100644 index 00000000..a9abaf0e --- /dev/null +++ b/packages/module/src/tracking/segment_tracking_provider.ts @@ -0,0 +1,58 @@ +import { AnalyticsBrowser } from '@segment/analytics-next'; + +import { TrackingApi, TrackingEventProperties } from './tracking_api'; +import { InitProps, TrackingSpi } from './tracking_spi'; + +export class SegmentTrackingProvider implements TrackingSpi, TrackingApi { + private analytics: AnalyticsBrowser | undefined; + getKey(): string { + return 'segmentKey'; + } + + initialize(props: InitProps): void { + // eslint-disable-next-line no-console + console.log('SegmentProvider initialize'); + const segmentKey = props.segmentKey as string; + + this.analytics = AnalyticsBrowser.load( + { + writeKey: segmentKey, + cdnURL: props.segmentCdn as string + }, + + { + integrations: { + ...props.segmentIntegrations + } + } + ); + } + + identify(userID: string): void { + // eslint-disable-next-line no-console + console.log('SegmentProvider userID: ' + userID); + if (this.analytics) { + this.analytics.identify(userID); + } + } + + trackPageView(url: string | undefined): void { + // eslint-disable-next-line no-console + console.log('SegmentProvider url', url); + if (this.analytics) { + if (url) { + this.analytics.page(url); + } else { + this.analytics.page(); // Uses window.url + } + } + } + + trackSingleItem(item: string, properties?: TrackingEventProperties): void { + // eslint-disable-next-line no-console + console.log('SegmentProvider: trackSingleItem' + item, properties); + if (this.analytics) { + this.analytics.track(item, { properties }); + } + } +} diff --git a/packages/module/src/tracking/trackingProviderProxy.ts b/packages/module/src/tracking/trackingProviderProxy.ts new file mode 100644 index 00000000..e2c78d5c --- /dev/null +++ b/packages/module/src/tracking/trackingProviderProxy.ts @@ -0,0 +1,28 @@ +import { TrackingApi, TrackingEventProperties } from './tracking_api'; +class TrackingProviderProxy implements TrackingApi { + providers: TrackingApi[] = []; + + constructor(providers: TrackingApi[]) { + this.providers = providers; + } + + identify(userID: string): void { + for (const provider of this.providers) { + provider.identify(userID); + } + } + + trackSingleItem(eventName: string, properties?: TrackingEventProperties): void { + for (const provider of this.providers) { + provider.trackSingleItem(eventName, properties); + } + } + + trackPageView(url: string | undefined): void { + for (const provider of this.providers) { + provider.trackPageView(url); + } + } +} + +export default TrackingProviderProxy; diff --git a/packages/module/src/tracking/tracking_api.ts b/packages/module/src/tracking/tracking_api.ts new file mode 100644 index 00000000..fef0fdc7 --- /dev/null +++ b/packages/module/src/tracking/tracking_api.ts @@ -0,0 +1,11 @@ +export interface TrackingEventProperties { + [key: string]: string | number | boolean | undefined; +} + +export interface TrackingApi { + identify: (userID: string) => void; + + trackPageView: (url: string | undefined) => void; + + trackSingleItem: (eventName: string, properties: TrackingEventProperties | undefined) => void; +} diff --git a/packages/module/src/tracking/tracking_registry.ts b/packages/module/src/tracking/tracking_registry.ts new file mode 100644 index 00000000..6f35bedc --- /dev/null +++ b/packages/module/src/tracking/tracking_registry.ts @@ -0,0 +1,33 @@ +import { InitProps, TrackingSpi } from './tracking_spi'; +import { TrackingApi } from './tracking_api'; +import TrackingProviderProxy from './trackingProviderProxy'; +import { ConsoleTrackingProvider } from './console_tracking_provider'; +import { SegmentTrackingProvider } from './segment_tracking_provider'; +import { PosthogTrackingProvider } from './posthog_tracking_provider'; +import { UmamiTrackingProvider } from './umami_tracking_provider'; + +export const getTrackingProviders = (initProps: InitProps): TrackingApi => { + const providers: TrackingSpi[] = []; + providers.push(new SegmentTrackingProvider()); + providers.push(new PosthogTrackingProvider()); + providers.push(new UmamiTrackingProvider()); + + // TODO dynamically find and register providers + + // Initialize them + const enabledProviders: TrackingSpi[] = []; + for (const provider of providers) { + const key = provider.getKey(); + if (Object.keys(initProps).indexOf(key) > -1) { + provider.initialize(initProps); + enabledProviders.push(provider); + } + } + // Add the console provider + const consoleTrackingProvider = new ConsoleTrackingProvider(); + enabledProviders.push(consoleTrackingProvider); // TODO noop- provider? + + return new TrackingProviderProxy(enabledProviders); +}; + +export default getTrackingProviders; diff --git a/packages/module/src/tracking/tracking_spi.ts b/packages/module/src/tracking/tracking_spi.ts new file mode 100644 index 00000000..7045dc14 --- /dev/null +++ b/packages/module/src/tracking/tracking_spi.ts @@ -0,0 +1,14 @@ +import { TrackingApi, TrackingEventProperties } from './tracking_api'; + +export interface InitProps { + [key: string]: string | number | boolean; +} + +export interface TrackingSpi extends TrackingApi { + // Return a key in InitProps to check if the provided should be enabled + getKey: () => string; + // Initialize the provider + initialize: (props: InitProps) => void; + // Track a single item + trackSingleItem: (item: string, properties?: TrackingEventProperties) => void; +} diff --git a/packages/module/src/tracking/umami_tracking_provider.ts b/packages/module/src/tracking/umami_tracking_provider.ts new file mode 100644 index 00000000..f13d03fd --- /dev/null +++ b/packages/module/src/tracking/umami_tracking_provider.ts @@ -0,0 +1,53 @@ +import { InitProps, TrackingSpi } from './tracking_spi'; +import { TrackingApi, TrackingEventProperties } from './tracking_api'; + +declare global { + interface Window { + umami: any; + } +} + +export class UmamiTrackingProvider implements TrackingSpi, TrackingApi { + getKey(): string { + return 'umamiKey'; + } + + initialize(props: InitProps): void { + // eslint-disable-next-line no-console + console.log('UmamiProvider initialize'); + const umamiKey = props.umamiKey as string; + const hostUrl = props.umamiHostUrl as string; + + const script = document.createElement('script'); + script.src = hostUrl + '/script.js'; + script.async = true; + script.defer = true; + + // Configure Umami properties + script.setAttribute('data-website-id', umamiKey); + script.setAttribute('data-domains', 'localhost'); // TODO ? + script.setAttribute('data-auto-track', 'false'); + script.setAttribute('data-host-url', hostUrl); // TODO ? + script.setAttribute('data-exclude-search', 'false'); // TODO ? + + document.body.appendChild(script); + } + + identify(userID: string): void { + // eslint-disable-next-line no-console + console.log('UmamiProvider userID: ' + userID); + window.umami?.identify({ userID }); + } + + trackPageView(url: string | undefined): void { + // eslint-disable-next-line no-console + console.log('UmamiProvider url', url); + window.umami?.track({ url }); + } + + trackSingleItem(item: string, properties?: TrackingEventProperties): void { + // eslint-disable-next-line no-console + console.log('UmamiProvider: trackSingleItem' + item, properties); + window.umami?.track(item, properties); + } +}