Skip to content

Commit 0e86fed

Browse files
authored
[Analytics] Track install and error events (#1984)
This PR will track which plugins and themes were installed during boot from a Blueprint, by sending a `installTheme` and `installPlugin` events. An `error` event will be sent if Playground experiences a boot or PHP request error. All events are sent to Google Analytics and don't contain any personal data information. `InstallTheme` and `InstallPlugin` events will include the resource type and the theme/plugin slug if the resource type is WordPress.org. `Error` events will only send an error `source` which can be: - `bootSiteClient` - error occurred during boot. - `request` - a PHP or SQL error occurred during a PHP request. - `unknown` - for request errors that don't have a source. This shouldn't happen but we still want to catch it to confirm it's not happening in practice. The purpose of the `error` event is to provide insights into how frequently users experience issues. It's not intended for logging or debugging. Fixes [#1778](#1778) ## Testing Instructions (or ideally a Blueprint) - The easiest way to see events is to add a `console.log` before this line https://github.com/WordPress/wordpress-playground/blob/acea593bc551aac14aebd982333be592ee4b298c/packages/playground/website/src/lib/tracking.ts#L26 - Open this link to [http://127.0.0.1:5400/website-server/?plugin=friends&theme=twentytwentythree](http://127.0.0.1:5400/website-server/?plugin=friends&theme=twentytwentythree) - Confirm that a `installPlugin` and `installTheme` install event were sent with the slug and correct resource type - Open this link to [install a plugin from a git resource.](http://127.0.0.1:5400/website-server/#{%22steps%22:%20[{%20%22step%22:%20%22installPlugin%22,%20%22pluginData%22:%20{%20%22resource%22:%20%22git:directory%22,%20%22url%22:%20%22https://github.com/WordPress/block-development-examples%22,%20%22ref%22:%20%22HEAD%22,%20%22path%22:%20%22plugins/data-basics-59c8f8%22%20},%20%22options%22:%20{%20%22activate%22:%20true%20}%20}]}) - Confirm that a `installPlugin` event was sent with the correct resource type - Open this link [http://127.0.0.1:5400/website-server/?plugin=friefffnds&theme=astra](http://127.0.0.1:5400/website-server/?plugin=friefffnds&theme=astra) - Confirm that at `bootSiteClient` error was sent
1 parent e51665e commit 0e86fed

File tree

5 files changed

+70
-24
lines changed

5 files changed

+70
-24
lines changed

packages/playground/blueprints/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export {
2525
// BC:
2626
compileBlueprintV1 as compileBlueprint,
2727
runBlueprintV1Steps as runBlueprintSteps,
28+
isStepDefinition,
2829
} from './lib/v1/compile';
2930
export type {
3031
CompileBlueprintV1Options,

packages/playground/blueprints/src/lib/v1/compile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ function compileVersion<T>(
607607
* @param step The object to test
608608
* @returns Whether the object is a StepDefinition
609609
*/
610-
function isStepDefinition(
610+
export function isStepDefinition(
611611
step: Step | string | undefined | false | null
612612
): step is StepDefinition {
613613
return !!(typeof step === 'object' && step);

packages/playground/website/src/components/layout/index.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CSSTransition } from 'react-transition-group';
55
import type { PlaygroundReduxState } from '../../lib/state/redux/store';
66
import { useAppSelector } from '../../lib/state/redux/store';
77
import type { BlueprintV1Declaration } from '@wp-playground/blueprints';
8-
import { useState, useEffect, useRef } from 'react';
8+
import { useState, useRef } from 'react';
99
import { acquireOAuthTokenIfNeeded } from '../../github/acquire-oauth-token-if-needed';
1010
import { GithubExportModal } from '../../github/github-export-form';
1111
import type { ExportFormValues } from '../../github/github-export-form/form';
@@ -125,9 +125,6 @@ function Modals(blueprint: BlueprintV1Declaration) {
125125
return values;
126126
});
127127

128-
// eslint-disable-next-line react-hooks/exhaustive-deps
129-
useEffect(() => {}, []);
130-
131128
const currentModal = useAppSelector(
132129
(state: PlaygroundReduxState) => state.ui.activeModal
133130
);

packages/playground/website/src/lib/state/redux/boot-site-client.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
removeClientInfo,
1010
updateClientInfo,
1111
} from './slice-clients';
12-
import { logTrackingEvent } from '../../tracking';
12+
import { logBlueprintEvents, logTrackingEvent } from '../../tracking';
1313
import {
1414
type Blueprint,
1515
BlueprintFilesystemRequiredError,
@@ -150,16 +150,8 @@ export function bootSiteClient(
150150
playground = (window as any)['playground'] =
151151
playgroundClient;
152152
},
153-
// Log the names of provided Blueprint's steps.
154-
// Only the names (e.g. "runPhp" or "login") are logged. Step options like
155-
// code, password, URLs are never sent anywhere.
156-
onBlueprintValidated: (blueprint) => {
157-
for (const step of blueprint.steps || []) {
158-
if (typeof step === 'object' && step?.step) {
159-
logTrackingEvent('step', { step: step.step });
160-
}
161-
}
162-
},
153+
// Log Blueprint events
154+
onBlueprintValidated: logBlueprintEvents,
163155
mounts: mountDescriptor
164156
? [
165157
{
@@ -174,6 +166,7 @@ export function bootSiteClient(
174166
});
175167
} catch (e) {
176168
logger.error(e);
169+
logTrackingEvent('error', { source: 'bootSiteClient' });
177170

178171
if (
179172
(e as any).name === 'ArtifactExpiredError' ||
Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,78 @@
1+
import type { BlueprintV1 } from '@wp-playground/blueprints';
2+
import {
3+
getBlueprintDeclaration,
4+
isStepDefinition,
5+
} from '@wp-playground/blueprints';
6+
import { logger } from '@php-wasm/logger';
17
/**
28
* Declare the global window.gtag function
39
*/
410
declare global {
5-
interface Window { gtag: any; }
11+
interface Window {
12+
gtag: any;
13+
}
614
}
715

816
/**
917
* Google Analytics event names
1018
*/
11-
type GAEvent = 'load' | 'step';
19+
type GAEvent = 'load' | 'step' | 'installPlugin' | 'installTheme' | 'error';
1220

1321
/**
1422
* Log a tracking event to Google Analytics
1523
* @param GAEvent The event name
1624
* @param Object Event data
1725
*/
18-
export const logTrackingEvent = (event: GAEvent, data?: {[key: string]: string}) => {
19-
if (typeof window === 'undefined' || !window.gtag) {
20-
return;
21-
}
22-
window.gtag('event', event, data);
23-
}
26+
export const logTrackingEvent = (
27+
event: GAEvent,
28+
data?: { [key: string]: string }
29+
) => {
30+
try {
31+
if (typeof window === 'undefined' || !window.gtag) {
32+
return;
33+
}
34+
window.gtag('event', event, data);
35+
} catch (error) {
36+
logger.warn('Failed to log tracking event', event, data, error);
37+
}
38+
};
39+
40+
/**
41+
* Log Blueprint events
42+
* @param blueprint The Blueprint
43+
*/
44+
export const logBlueprintEvents = async (blueprint: BlueprintV1) => {
45+
/**
46+
* Log the names of provided Blueprint steps.
47+
* Only the names (e.g. "runPhp" or "login") are logged. Step options like
48+
* code, password, URLs are never sent anywhere.
49+
*
50+
* For installPlugin and installTheme, the plugin/theme slug is logged.
51+
*/
52+
const blueprintDeclaration = await getBlueprintDeclaration(blueprint);
53+
if (blueprintDeclaration.steps) {
54+
for (const step of blueprintDeclaration.steps) {
55+
if (!isStepDefinition(step)) {
56+
continue;
57+
}
58+
logTrackingEvent('step', { step: step.step });
59+
if (step.step === 'installPlugin') {
60+
const data = {
61+
resource: (step as any).pluginData.resource,
62+
};
63+
if ((step as any).pluginData.slug) {
64+
(data as any).plugin = (step as any).pluginData.slug;
65+
}
66+
logTrackingEvent('installPlugin', data);
67+
} else if (step.step === 'installTheme') {
68+
const data = {
69+
resource: (step as any).themeData.resource,
70+
};
71+
if ((step as any).themeData.slug) {
72+
(data as any).theme = (step as any).themeData.slug;
73+
}
74+
logTrackingEvent('installTheme', data);
75+
}
76+
}
77+
}
78+
};

0 commit comments

Comments
 (0)