diff --git a/.eslintrc.json b/.eslintrc.json index 0b937e277..5b151c157 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -62,7 +62,7 @@ "dot-notation": "error", "eqeqeq": ["error", "smart"], "guard-for-in": "error", - "indent": ["error", 2], + "indent": ["error", 2, {"SwitchCase": 1}], "max-classes-per-file": ["error", 1], "no-nested-ternary": "error", "no-bitwise": "error", 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 index bb60172d1..370329ef3 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md @@ -36,10 +36,12 @@ Note that user code only interacts with: ### Setup -1. Before you can use the `trackingAPI`, you must first supply the API keys of the respective providers. +1. Before you can use the `trackingAPI`, you must first supply the API keys of the respective providers. To enable a provider, it must be added to the `activeProviders` property: ```nolive const initProps: InitProps = { + verbose: false, + activeProviders: ['Segment', 'Umami', 'Posthog', 'Console' ], segmentKey: 'TODO-key', // TODO add your key here // segmentCdn: 'https://my.org/cdn', // Set up segment cdn (optional) // segmentIntegrations: { // Provide Segment integrations (optional) @@ -50,13 +52,15 @@ const initProps: InitProps = { }, posthogKey: 'TODO-key', - umamiKey: 'TODO-key', + umamiKey: 'TODO-umami-key', umamiHostUrl: 'http://localhost:3000', // TODO where is your JS provider? + 'umami-data-domains': 'TODO umami data domain', something: 'test', - console: 'true' // Console provider }; ``` +- **Note:** To enable output debugging via the web-browser console, set the `verbose` key to `true`. By default, this is set to `false`. + 1. Once this is done, you can create an instance of the `trackingAPI` and start sending events. ```nolive @@ -76,22 +80,20 @@ trackingAPI.trackSingleItem("MyEvent", { response: 'Good response' }) #### Tracking providers -Only providers with a matching key in the `InitProps` will be started and used. +Only providers with a matching entry in the `InitProps.activeProviders` array will be started and used. + +Possible values are: +* Umami +* Posthog +* Segment +* Console -```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 -``` ##### Modifying providers If you know upfront that you only want to use 1 of the supported providers, you can modify `getTrackingProviders()` and remove all other providers in the providers array. -When using the providers you need to add additional dependencies to your package.json file: +When using the providers, you might need to add additional dependencies to your package.json file: ```nolive "dependencies": { @@ -99,12 +101,14 @@ When using the providers you need to add additional dependencies to your package "posthog-js": "^1.194.4" ``` +Depending on your local setup, this might not be necessary. For example, if you pull the ChatBot codebase as a dependency into your project, you don't need to add it as an additional dependency in your package.json. + ##### Adding providers To add another analytics provider, you need to implement 2 interfaces, `TrackingSpi` and `trackingApi`. 1. It is easiest to start by copying the `ConsoleTrackingProvider` -1. The first thing you should do is to provide a correct value in `getKey()` +1. Add an entry for your new provider to the `Providers` enum in `tracking_spi.ts` 1. Once you are happy enough with the implementation, add it to the array of providers in `getTrackingProviders()` ### Page flow tracking 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 dd4133296..cc302b99c 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 @@ -98,17 +98,18 @@ export default MessageLoading; const date = new Date(); const initProps: InitProps = { + verbose: false, segmentKey: 'TODO-key', // TODO add your key here posthogKey: 'TODO-key', umamiKey: 'TODO-key', - umamiHostUrl: 'http://localhost:3000', // TODO where is your JS provider? + umamiHostUrl: 'http://localhost:3000', // TODO where is your Umami installation? console: true, something: 'test' }; const tracking = getTrackingProviders(initProps); -tracking.identify('user-123'); // TODO get real user id -tracking.trackPageView(window.document.documentURI); +tracking.identify('user-123', { superUser: true }); // TODO get real user id + properties +tracking.trackPageView(window.location.href); const actionEventName = 'MessageAction'; const initialMessages: MessageProps[] = [ diff --git a/packages/module/src/tracking/console_tracking_provider.ts b/packages/module/src/tracking/console_tracking_provider.ts index 178b25b78..4c9555a56 100644 --- a/packages/module/src/tracking/console_tracking_provider.ts +++ b/packages/module/src/tracking/console_tracking_provider.ts @@ -1,30 +1,34 @@ -import { TrackingSpi } from './tracking_spi'; +import { InitProps, TrackingSpi } from './tracking_spi'; import { TrackingApi, TrackingEventProperties } from './tracking_api'; export class ConsoleTrackingProvider implements TrackingSpi, TrackingApi { + private verbose = false; trackPageView(url: string | undefined) { - // eslint-disable-next-line no-console - console.log('ConsoleProvider pageView', url); + if (this.verbose) { + // 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'); + initialize(props: InitProps): void { + this.verbose = props.verbose; + if (this.verbose) { + // 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); + identify(userID: string, userProperties: TrackingEventProperties = {}): void { + if (this.verbose) { + // eslint-disable-next-line no-console + console.log('ConsoleProvider identify ', userID, userProperties); + } } trackSingleItem(item: string, properties?: TrackingEventProperties): void { - // eslint-disable-next-line no-console - console.log('ConsoleProvider: ' + item, properties); - } - - getKey(): string { - return 'console'; + if (this.verbose) { + // eslint-disable-next-line no-console + console.log('ConsoleProvider: ' + item, properties); + } } } diff --git a/packages/module/src/tracking/posthog_tracking_provider.ts b/packages/module/src/tracking/posthog_tracking_provider.ts index fb8c7b934..d97d3a2a5 100644 --- a/packages/module/src/tracking/posthog_tracking_provider.ts +++ b/packages/module/src/tracking/posthog_tracking_provider.ts @@ -4,13 +4,14 @@ import { TrackingApi, TrackingEventProperties } from './tracking_api'; import { InitProps, TrackingSpi } from './tracking_spi'; export class PosthogTrackingProvider implements TrackingSpi, TrackingApi { - getKey(): string { - return 'posthogKey'; - } + private verbose = false; initialize(props: InitProps): void { - // eslint-disable-next-line no-console - console.log('PosthogProvider initialize'); + this.verbose = props.verbose; + if (this.verbose) { + // eslint-disable-next-line no-console + console.log('PosthogProvider initialize'); + } const posthogKey = props.posthogKey as string; posthog.init(posthogKey, { @@ -21,22 +22,28 @@ export class PosthogTrackingProvider implements TrackingSpi, TrackingApi { }); } - identify(userID: string): void { - // eslint-disable-next-line no-console - console.log('PosthogProvider userID: ' + userID); - posthog.identify(userID); + identify(userID: string, userProperties: TrackingEventProperties = {}): void { + if (this.verbose) { + // eslint-disable-next-line no-console + console.log('PosthogProvider userID: ' + userID); + } + posthog.identify(userID, userProperties); } trackPageView(url: string | undefined): void { - // eslint-disable-next-line no-console - console.log('PostHogProvider url', url); + if (this.verbose) { + // 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); + if (this.verbose) { + // 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 index bbeb696c0..d58de53af 100644 --- a/packages/module/src/tracking/segment_tracking_provider.ts +++ b/packages/module/src/tracking/segment_tracking_provider.ts @@ -5,13 +5,14 @@ import { InitProps, TrackingSpi } from './tracking_spi'; export class SegmentTrackingProvider implements TrackingSpi, TrackingApi { private analytics: AnalyticsBrowser | undefined; - getKey(): string { - return 'segmentKey'; - } + private verbose = false; initialize(props: InitProps): void { - // eslint-disable-next-line no-console - console.log('SegmentProvider initialize'); + this.verbose = props.verbose; + if (this.verbose) { + // eslint-disable-next-line no-console + console.log('SegmentProvider initialize'); + } const segmentKey = props.segmentKey as string; // We need to create an object here, as ts lint is unhappy otherwise @@ -32,17 +33,21 @@ export class SegmentTrackingProvider implements TrackingSpi, TrackingApi { ); } - identify(userID: string): void { - // eslint-disable-next-line no-console - console.log('SegmentProvider userID: ' + userID); + identify(userID: string, userProperties: TrackingEventProperties = {}): void { + if (this.verbose) { + // eslint-disable-next-line no-console + console.log('SegmentProvider userID: ' + userID); + } if (this.analytics) { - this.analytics.identify(userID); + this.analytics.identify(userID, userProperties); } } trackPageView(url: string | undefined): void { - // eslint-disable-next-line no-console - console.log('SegmentProvider url', url); + if (this.verbose) { + // eslint-disable-next-line no-console + console.log('SegmentProvider url ', url); + } if (this.analytics) { if (url) { this.analytics.page(url); @@ -53,8 +58,10 @@ export class SegmentTrackingProvider implements TrackingSpi, TrackingApi { } trackSingleItem(item: string, properties?: TrackingEventProperties): void { - // eslint-disable-next-line no-console - console.log('SegmentProvider: trackSingleItem' + item, properties); + if (this.verbose) { + // 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 index ef25cd935..974a32abb 100644 --- a/packages/module/src/tracking/trackingProviderProxy.ts +++ b/packages/module/src/tracking/trackingProviderProxy.ts @@ -6,9 +6,9 @@ class TrackingProviderProxy implements TrackingApi { this.providers = providers; } - identify(userID: string): void { + identify(userID: string, userProperties: TrackingEventProperties = {}): void { for (const provider of this.providers) { - provider.identify(userID); + provider.identify(userID, userProperties); } } diff --git a/packages/module/src/tracking/tracking_api.ts b/packages/module/src/tracking/tracking_api.ts index fef0fdc7b..5c41aa250 100644 --- a/packages/module/src/tracking/tracking_api.ts +++ b/packages/module/src/tracking/tracking_api.ts @@ -3,7 +3,7 @@ export interface TrackingEventProperties { } export interface TrackingApi { - identify: (userID: string) => void; + identify: (userID: string, userProperties: TrackingEventProperties) => void; trackPageView: (url: string | undefined) => void; diff --git a/packages/module/src/tracking/tracking_registry.ts b/packages/module/src/tracking/tracking_registry.ts index 6f35bedc5..60929667b 100644 --- a/packages/module/src/tracking/tracking_registry.ts +++ b/packages/module/src/tracking/tracking_registry.ts @@ -1,4 +1,4 @@ -import { InitProps, TrackingSpi } from './tracking_spi'; +import { InitProps, Providers, TrackingSpi } from './tracking_spi'; import { TrackingApi } from './tracking_api'; import TrackingProviderProxy from './trackingProviderProxy'; import { ConsoleTrackingProvider } from './console_tracking_provider'; @@ -8,26 +8,59 @@ 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 + if (initProps.activeProviders) { + let tmpProps: string[] = initProps.activeProviders; + + // Theoretically we get an array of provider names, but it could also be a CSV string... + if (!Array.isArray(initProps.activeProviders)) { + const tmpString = initProps.activeProviders as string; + if (tmpString && tmpString.indexOf(',') !== -1) { + tmpProps = tmpString.split(','); + } else { + tmpProps = [tmpString]; + } + } + + tmpProps.forEach((provider) => { + switch (Providers[provider]) { + case Providers.Segment: + providers.push(new SegmentTrackingProvider()); + break; + case Providers.Umami: + providers.push(new UmamiTrackingProvider()); + break; + case Providers.Posthog: + providers.push(new PosthogTrackingProvider()); + break; + case Providers.Console: + providers.push(new ConsoleTrackingProvider()); + break; + case Providers.None: // Do nothing, just a placeholder + break; + default: + if (providers.length > 1) { + if (initProps.verbose) { + // eslint-disable-next-line no-console + console.error("Unknown provider '" + provider); + } + } + break; + } + }); + } // Initialize them - const enabledProviders: TrackingSpi[] = []; for (const provider of providers) { - const key = provider.getKey(); - if (Object.keys(initProps).indexOf(key) > -1) { + try { provider.initialize(initProps); - enabledProviders.push(provider); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); } } - // Add the console provider - const consoleTrackingProvider = new ConsoleTrackingProvider(); - enabledProviders.push(consoleTrackingProvider); // TODO noop- provider? - return new TrackingProviderProxy(enabledProviders); + return new TrackingProviderProxy(providers); }; export default getTrackingProviders; diff --git a/packages/module/src/tracking/tracking_spi.ts b/packages/module/src/tracking/tracking_spi.ts index 7045dc141..ddbbae42e 100644 --- a/packages/module/src/tracking/tracking_spi.ts +++ b/packages/module/src/tracking/tracking_spi.ts @@ -1,14 +1,25 @@ -import { TrackingApi, TrackingEventProperties } from './tracking_api'; +import { TrackingApi } from './tracking_api'; -export interface InitProps { - [key: string]: string | number | boolean; +export enum Providers { + None, + Segment, + Umami, + Posthog, + Console +} + +export type ProviderAsString = keyof typeof Providers; + +export interface BaseProps { + verbose: boolean; + activeProviders: [ProviderAsString]; } +export type InitProps = { + [key: string]: string | number | boolean; +} & BaseProps; + 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 index 09a818216..f86d79063 100644 --- a/packages/module/src/tracking/umami_tracking_provider.ts +++ b/packages/module/src/tracking/umami_tracking_provider.ts @@ -8,15 +8,25 @@ declare global { } } +// Items in a queue. +// We need to queue up requests until the script is fully loaded +interface queueT { + what: 'i' | 't' | 'p'; // identify, track, pageview + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + payload?: any; +} + export class UmamiTrackingProvider implements TrackingSpi, TrackingApi { - getKey(): string { - return 'umamiKey'; - } + private verbose = false; + private websiteId: string | undefined; + private queue: queueT[] = []; initialize(props: InitProps): void { - // eslint-disable-next-line no-console - console.log('UmamiProvider initialize'); - const umamiKey = props.umamiKey as string; + this.verbose = props.verbose; + this.log('UmamiProvider initialize'); + + this.websiteId = props.umamiKey as string; const hostUrl = props.umamiHostUrl as string; const script = document.createElement('script'); @@ -25,30 +35,76 @@ export class UmamiTrackingProvider implements TrackingSpi, TrackingApi { script.defer = true; // Configure Umami properties - script.setAttribute('data-website-id', umamiKey); - script.setAttribute('data-domains', 'localhost'); // TODO ? + script.setAttribute('data-website-id', this.websiteId); + script.setAttribute('data-host-url', hostUrl); script.setAttribute('data-auto-track', 'false'); - script.setAttribute('data-host-url', hostUrl); // TODO ? - script.setAttribute('data-exclude-search', 'false'); // TODO ? + script.setAttribute('data-exclude-search', 'false'); + + // Now get from config, which may override some of the above. + const UMAMI_PREFIX = 'umami-'; + for (const prop in props) { + if (prop.startsWith(UMAMI_PREFIX)) { + const att = 'data-' + prop.substring(UMAMI_PREFIX.length); + const val = props[prop]; + script.setAttribute(att, String(val)); + } + } + script.onload = () => { + this.log('UmamiProvider script loaded'); + this.flushQueue(); + }; document.body.appendChild(script); } - identify(userID: string): void { - // eslint-disable-next-line no-console - console.log('UmamiProvider userID: ' + userID); - window.umami?.identify({ userID }); + identify(userID: string, userProperties: TrackingEventProperties = {}): void { + this.log('UmamiProvider userID: ' + userID + ' => ' + JSON.stringify(userProperties)); + if (window.umami) { + window.umami.identify({ userID, userProperties }); + } else { + this.queue.push({ what: 'i', name: userID, payload: userProperties }); + } } trackPageView(url: string | undefined): void { - // eslint-disable-next-line no-console - console.log('UmamiProvider url', url); - window.umami?.track({ url }); + this.log('UmamiProvider url ' + url); + if (window.umami) { + window.umami.track({ url, website: this.websiteId }); + } else { + this.queue.push({ what: 'p', name: String(url) }); + } } trackSingleItem(item: string, properties?: TrackingEventProperties): void { - // eslint-disable-next-line no-console - console.log('UmamiProvider: trackSingleItem' + item, properties); - window.umami?.track(item, properties); + this.log('UmamiProvider: trackSingleItem ' + item + JSON.stringify(properties)); + if (window.umami) { + window.umami.track(item, properties); + } else { + this.queue.push({ what: 't', name: item, payload: properties }); + } + } + + flushQueue(): void { + for (const item of this.queue) { + this.log('Queue flush ' + JSON.stringify(item)); + switch (item.what) { + case 'i': + this.identify(item.name, item.payload); + break; + case 't': + this.trackSingleItem(item.name, item.payload); + break; + case 'p': + this.trackPageView(item.name); + break; + } + } + } + + log(msg: string): void { + if (this.verbose) { + // eslint-disable-next-line no-console + console.debug('UmamiProvider: ', msg); + } } }