PluginProbe
Parse.ly / 3.14.1
Parse.ly v3.14.1
3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 3.20.3 All 105 releases
wp-parsely / src / js / telemetry / telemetry.ts

telemetry.ts in Parse.ly 3.14.1, at src/js/telemetry/telemetry.ts

339 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 declare global {
2 /**
3 * The global object containing the telemetry data.
4 *
5 * @since 3.12.0
6 */
7 const wpParselyTracksTelemetry: {
8 version: string,
9 vipgo_env?: string,
10 user: {
11 type: string,
12 id: string,
13 }
14 };
15
16 interface Window {
17 /**
18 * Singleton instance of the Telemetry class.
19 * This is attached to the global `window` object to ensure that the same instance
20 * is used across different ES modules in the application.
21 *
22 * @since 3.12.0
23 */
24 wpParselyTelemetryInstance: Telemetry;
25 _tkq: EventProps[];
26 }
27 }
28
29 /**
30 * Event properties.
31 *
32 * @since 3.12.0
33 */
34 export type EventProps = {
35 [ key: string ]: string|number|boolean;
36 }
37
38 /**
39 * Telemetry class.
40 *
41 * @since 3.12.0
42 */
43 export class Telemetry {
44 /**
45 * The prefix used for all events.
46 *
47 * @since 3.12.0
48 * @access private
49 */
50 private static readonly TRACKS_PREFIX = 'wpparsely_';
51
52 /**
53 * The regex used to validate event names.
54 *
55 * @since 3.12.0
56 * @access private
57 */
58 private static readonly EVENT_NAME_REGEX = /^(([a-z0-9]+)_){2}([a-z0-9_]+)$/;
59
60 /**
61 * The regex used to validate event properties.
62 *
63 * @since 3.12.0
64 * @access private
65 */
66 private static readonly PROPERTY_REGEX = /^[a-z_][a-z0-9_]*$/;
67
68 /**
69 * The queue of events to be tracked.
70 *
71 * @since 3.12.0
72 * @access private
73 */
74 private _tkq: ( string | object )[] = [];
75
76 /**
77 * Whether the tracking library has been loaded.
78 *
79 * @since 3.12.0
80 * @access protected
81 */
82 protected isLoaded: boolean = false;
83
84 /**
85 * Whether the tracking is enabled.
86 * Looks for the `wpParselyTracksTelemetry` global object. If it exists, telemetry is enabled.
87 *
88 * @since 3.12.0
89 * @access protected
90 */
91 protected isEnabled: boolean = false;
92
93 /**
94 * Private constructor to prevent direct object creation.
95 * This is necessary because this class is a singleton.
96 *
97 * @since 3.12.0
98 */
99 private constructor() {
100 if ( typeof wpParselyTracksTelemetry !== 'undefined' ) {
101 this.isEnabled = true;
102 this.loadTrackingLibrary();
103 }
104 }
105
106 /**
107 * Returns the singleton instance of the Telemetry class.
108 * If the instance does not exist, it is created.
109 *
110 * @since 3.12.0
111 *
112 * @return {Telemetry} The singleton instance of the Telemetry class.
113 */
114 public static getInstance(): Telemetry {
115 if ( ! window.wpParselyTelemetryInstance ) {
116 Object.defineProperty( window, 'wpParselyTelemetryInstance', {
117 value: new Telemetry(),
118 writable: false,
119 configurable: false,
120 enumerable: false, // This makes it not show up in console enumerations.
121 } );
122 }
123 return window.wpParselyTelemetryInstance;
124 }
125
126 /**
127 * Loads the tracking library.
128 *
129 * @since 3.12.0
130 */
131 private loadTrackingLibrary(): void {
132 const script = document.createElement( 'script' );
133 script.async = true;
134 script.src = '//stats.wp.com/w.js';
135 script.onload = () => {
136 this.isLoaded = true;
137 this._tkq = window._tkq || [];
138 };
139 document.head.appendChild( script );
140 }
141
142 /**
143 * Tracks an event.
144 * This method is static, so it can be called directly from the class.
145 * It first checks if the telemetry is enabled, and if not, it bails.
146 * Then, ensures that the telemetry library is loaded by calling `waitUntilLoaded`.
147 * Finally, it calls the `trackEvent` method on the singleton instance of the Telemetry class.
148 *
149 * @since 3.12.0
150 *
151 * @param {string} eventName The name of the event to track.
152 * @param {EventProps} properties The properties of the event to track.
153 *
154 * @return {Promise<void>} A Promise that resolves when the event has been tracked.
155 */
156 public static async trackEvent( eventName: string, properties: EventProps = {} ): Promise<void> {
157 const telemetry: Telemetry = Telemetry.getInstance();
158
159 // If telemetry is not enabled, bail.
160 if ( ! telemetry.isTelemetryEnabled() ) {
161 return;
162 }
163
164 await Telemetry.waitUntilLoaded();
165 telemetry.trackEvent( eventName, properties );
166 }
167
168 /**
169 * Waits until the telemetry library is loaded.
170 * This method is static, so it can be called directly from the class.
171 * It checks every 100ms if the telemetry library is loaded, and resolves when it is.
172 * If the library is not loaded after 10 seconds, it rejects.
173 *
174 * @since 3.12.0
175 *
176 * @return {Promise<void>} A Promise that resolves when the telemetry library is loaded.
177 */
178 public static waitUntilLoaded(): Promise<void> {
179 return new Promise( ( resolve, reject ) => {
180 const telemetry: Telemetry = Telemetry.getInstance();
181
182 if ( ! telemetry.isTelemetryEnabled() ) {
183 reject( 'Telemetry not enabled' );
184 return;
185 }
186
187 if ( telemetry.isLoaded ) {
188 resolve();
189 return;
190 }
191
192 let timeout = 0;
193
194 const interval = setInterval( () => {
195 if ( telemetry.isLoaded ) {
196 clearInterval( interval );
197 resolve();
198 }
199
200 timeout += 100;
201
202 if ( timeout >= 10000 ) {
203 clearInterval( interval );
204 reject( 'Telemetry library not loaded' );
205 }
206 }, 100 );
207 } );
208 }
209
210 /**
211 * Tracks an event.
212 * This method is called by the static `trackEvent` method.
213 * It first checks if the telemetry library is loaded.
214 * Then, it validates the event name and the event properties.
215 * Finally, it pushes the event to the `_tkq` array.
216 *
217 * @since 3.12.0
218 *
219 * @param {string} eventName The name of the event to track.
220 * @param {EventProps} properties The properties of the event to track.
221 */
222 private trackEvent( eventName: string, properties: EventProps ): void {
223 if ( ! this.isLoaded ) {
224 // eslint-disable-next-line no-console
225 console.error( 'Error tracking event: Telemetry not loaded' );
226 return;
227 }
228
229 // Validate if the event name has the correct prefix, if not, append it.
230 if ( eventName.indexOf( Telemetry.TRACKS_PREFIX ) !== 0 ) {
231 eventName = Telemetry.TRACKS_PREFIX + eventName;
232 }
233
234 // Validate the event name.
235 if ( ! this.isEventNameValid( eventName ) ) {
236 // eslint-disable-next-line no-console
237 console.error( 'Error tracking event: Invalid event name' );
238 return;
239 }
240
241 properties = this.prepareProperties( properties );
242
243 // Push the event to the queue.
244 this._tkq?.push( [ 'recordEvent', eventName, properties ] );
245 }
246
247 /**
248 * Checks if the telemetry is enabled.
249 *
250 * @since 3.12.0
251 */
252 public isTelemetryEnabled(): boolean {
253 return this.isEnabled;
254 }
255
256 /**
257 * Checks if a property is valid.
258 * A property is valid if it matches the PROPERTY_REGEX.
259 *
260 * @since 3.12.0
261 *
262 * @param {string} property The property to check.
263 *
264 * @return {boolean} `true` if the property is valid, `false` otherwise.
265 */
266 private isProprietyValid( property: string ): boolean {
267 return Telemetry.PROPERTY_REGEX.test( property );
268 }
269
270 /**
271 * Checks if an event name is valid.
272 * An event name is valid if it matches the EVENT_NAME_REGEX.
273 *
274 * @since 3.12.0
275 *
276 * @param {string} eventName The event name to check.
277 *
278 * @return {boolean} `true` if the event name is valid, `false` otherwise.
279 */
280 private isEventNameValid( eventName: string ): boolean {
281 return Telemetry.EVENT_NAME_REGEX.test( eventName );
282 }
283
284 /**
285 * Prepares the properties of an event.
286 * This method sanitizes the properties, sets the `parsely_version` property,
287 * and sets user-specific properties if they exist.
288 *
289 * @since 3.12.0
290 *
291 * @param {EventProps} properties The properties to prepare.
292 *
293 * @return {EventProps} The prepared properties.
294 */
295 private prepareProperties( properties: EventProps ): EventProps {
296 properties = this.sanitizeProperties( properties );
297
298 properties.parsely_version = wpParselyTracksTelemetry.version;
299
300 // Set user-specific properties.
301 if ( wpParselyTracksTelemetry.user ) {
302 properties._ut = wpParselyTracksTelemetry.user.type;
303 properties._ui = wpParselyTracksTelemetry.user.id;
304 }
305
306 // If VIP environment, set the vipgo_env property.
307 if ( wpParselyTracksTelemetry.vipgo_env ) {
308 properties.vipgo_env = wpParselyTracksTelemetry.vipgo_env;
309 }
310
311 return this.sanitizeProperties( properties );
312 }
313
314 /**
315 * Sanitizes the properties of an event.
316 * This method creates a new object and copies over all valid properties
317 * from the original properties.
318 *
319 * @since 3.12.0
320 *
321 * @param {EventProps} properties The properties to sanitize.
322 *
323 * @return {EventProps} The sanitized properties.
324 */
325 private sanitizeProperties( properties: EventProps ): EventProps {
326 const sanitizedProperties: EventProps = {};
327
328 Object.keys( properties ).forEach( ( property: string ) => {
329 if ( this.isProprietyValid( property ) ) {
330 sanitizedProperties[ property ] = properties[ property ];
331 }
332 } );
333
334 return sanitizedProperties;
335 }
336 }
337
338 export const trackEvent = Telemetry.trackEvent;
339