diff --git a/packages/playground/blueprints/src/index.ts b/packages/playground/blueprints/src/index.ts index 579fd0b317..cdc21bcbf5 100644 --- a/packages/playground/blueprints/src/index.ts +++ b/packages/playground/blueprints/src/index.ts @@ -3,7 +3,11 @@ import '@php-wasm/node-polyfills'; export * from './lib/steps'; export * from './lib/steps/handlers'; -export { runBlueprintSteps, compileBlueprint } from './lib/compile'; +export { + runBlueprintSteps, + compileBlueprint, + isStepDefinition, +} from './lib/compile'; export type { Blueprint, PHPConstants } from './lib/blueprint'; export type { CompiledStep, diff --git a/packages/playground/blueprints/src/lib/compile.ts b/packages/playground/blueprints/src/lib/compile.ts index 932522e448..254d267dc5 100644 --- a/packages/playground/blueprints/src/lib/compile.ts +++ b/packages/playground/blueprints/src/lib/compile.ts @@ -405,7 +405,7 @@ function compileVersion( * @param step The object to test * @returns Whether the object is a StepDefinition */ -function isStepDefinition( +export function isStepDefinition( step: Step | string | undefined | false | null ): step is StepDefinition { return !!(typeof step === 'object' && step); diff --git a/packages/playground/website/src/components/layout/index.tsx b/packages/playground/website/src/components/layout/index.tsx index bdd9c23ef7..5f6801dbc2 100644 --- a/packages/playground/website/src/components/layout/index.tsx +++ b/packages/playground/website/src/components/layout/index.tsx @@ -32,6 +32,7 @@ import { setActiveModal, setSiteManagerOpen, } from '../../lib/state/redux/slice-ui'; +import { logErrorEvent } from '../../lib/tracking'; import { ImportFormModal } from '../import-form-modal'; import { PreviewPRModal } from '../../github/preview-pr'; import { MissingSiteModal } from '../missing-site-modal'; @@ -164,6 +165,8 @@ function Modals(blueprint: Blueprint) { useEffect(() => { addCrashListener(logger, (e) => { const error = e as CustomEvent; + logErrorEvent(error.detail.source ?? 'unknown'); + if (error.detail?.source === 'php-wasm') { dispatch(setActiveModal(modalSlugs.ERROR_REPORT)); } diff --git a/packages/playground/website/src/lib/state/redux/boot-site-client.ts b/packages/playground/website/src/lib/state/redux/boot-site-client.ts index 6ae9b4664e..c043898c6b 100644 --- a/packages/playground/website/src/lib/state/redux/boot-site-client.ts +++ b/packages/playground/website/src/lib/state/redux/boot-site-client.ts @@ -10,8 +10,12 @@ import { removeClientInfo, updateClientInfo, } from './slice-clients'; -import { logTrackingEvent } from '../../tracking'; -import { Blueprint, StepDefinition } from '@wp-playground/blueprints'; +import { + logBlueprintEvents, + logErrorEvent, + logTrackingEvent, +} from '../../tracking'; +import { Blueprint } from '@wp-playground/blueprints'; import { logger } from '@php-wasm/logger'; import { setupPostMessageRelay } from '@php-wasm/web'; import { startPlaygroundWeb } from '@wp-playground/client'; @@ -93,26 +97,16 @@ export function bootSiteClient( } } - logTrackingEvent('load'); - let blueprint: Blueprint; if (isWordPressInstalled) { blueprint = site.metadata.runtimeConfiguration; } else { blueprint = site.metadata.originalBlueprint; - // Log the names of provided Blueprint's steps. - // Only the names (e.g. "runPhp" or "login") are logged. Step options like - // code, password, URLs are never sent anywhere. - const steps = (blueprint?.steps || []) - ?.filter( - (step: any) => !!(typeof step === 'object' && step?.step) - ) - .map((step) => (step as StepDefinition).step); - for (const step of steps) { - logTrackingEvent('step', { step }); - } } + logTrackingEvent('load'); + logBlueprintEvents(blueprint); + let playground: PlaygroundClient; try { playground = await startPlaygroundWeb({ @@ -177,6 +171,7 @@ export function bootSiteClient( } } catch (e) { logger.error(e); + logErrorEvent('bootSiteClient'); dispatch(setActiveSiteError('site-boot-failed')); dispatch(setActiveModal(modalSlugs.ERROR_REPORT)); return; diff --git a/packages/playground/website/src/lib/tracking.ts b/packages/playground/website/src/lib/tracking.ts index 1dbc0b85a0..4629a3d3f6 100644 --- a/packages/playground/website/src/lib/tracking.ts +++ b/packages/playground/website/src/lib/tracking.ts @@ -1,23 +1,113 @@ +import { Blueprint, isStepDefinition } from '@wp-playground/blueprints'; +import { logger } from '@php-wasm/logger'; + /** * Declare the global window.gtag function */ declare global { - interface Window { gtag: any; } + interface Window { + gtag: any; + } } /** * Google Analytics event names */ -type GAEvent = 'load' | 'step'; +type GAEvent = 'load' | 'step' | 'installPlugin' | 'installTheme' | 'error'; /** * Log a tracking event to Google Analytics * @param GAEvent The event name * @param Object Event data */ -export const logTrackingEvent = (event: GAEvent, data?: {[key: string]: string}) => { - if (typeof window === 'undefined' || !window.gtag) { - return; - } - window.gtag('event', event, data); -} +export const logTrackingEvent = ( + event: GAEvent, + data?: { [key: string]: string } +) => { + try { + if (typeof window === 'undefined' || !window.gtag) { + return; + } + window.gtag('event', event, data); + } catch (error) { + logger.warn('Failed to log tracking event', event, data, error); + } +}; + +/** + * Log error events + * + * @param error The error + */ +export const logErrorEvent = (source: string) => { + logTrackingEvent('error', { + source, + }); +}; + +/** + * Log plugin install events + * @param slug The plugin slug + */ +export const logPluginInstallEvent = (slug: string) => { + logTrackingEvent('installPlugin', { + plugin: slug, + }); +}; + +/** + * Log theme install events + * @param slug The theme slug + */ +export const logThemeInstallEvent = (slug: string) => { + logTrackingEvent('installTheme', { + theme: slug, + }); +}; + +/** + * Log Blueprint events + * @param blueprint The Blueprint + */ +export const logBlueprintEvents = (blueprint: Blueprint) => { + /** + * Log the names of provided Blueprint steps. + * Only the names (e.g. "runPhp" or "login") are logged. Step options like + * code, password, URLs are never sent anywhere. + * + * For installPlugin and installTheme, the plugin/theme slug is logged. + */ + if (blueprint.steps) { + for (const step of blueprint.steps) { + if (!isStepDefinition(step)) { + continue; + } + logTrackingEvent('step', { step: step.step }); + if ( + step.step === 'installPlugin' && + (step as any).pluginData.slug + ) { + logPluginInstallEvent((step as any).pluginData.slug); + } else if ( + step.step === 'installTheme' && + (step as any).themeData.slug + ) { + logThemeInstallEvent((step as any).themeData.slug); + } + } + } + + /** + * Because the Blueprint isn't compiled, we need to log the plugins + * that are installed using the `plugins` shorthand. + */ + if (blueprint.plugins) { + for (const plugin of blueprint.plugins) { + if (typeof plugin !== 'string') { + continue; + } + logTrackingEvent('step', { step: 'installPlugin' }); + logPluginInstallEvent(plugin); + } + } +};