From 3fff8326470bbf6ceb1a89710db0b9869a8af745 Mon Sep 17 00:00:00 2001 From: Michael Carenzo <79934822+mikecarenzo@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:52:13 -0400 Subject: [PATCH 1/5] add baseline analytics --- src/tie-web-interface/index.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tie-web-interface/index.html b/src/tie-web-interface/index.html index fe4915f..dd4a110 100644 --- a/src/tie-web-interface/index.html +++ b/src/tie-web-interface/index.html @@ -11,6 +11,14 @@ + + From 9d673a485c35ab8f516ab5e65f41c9cd12c9aedd Mon Sep 17 00:00:00 2001 From: Michael Carenzo <79934822+mikecarenzo@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:28:38 -0400 Subject: [PATCH 2/5] adds application-specific analytics --- .../scripts/Application/EventRecorder.ts | 118 ++++++++++++++++++ .../scripts/Application/EventStorage.ts | 12 ++ .../scripts/Application/GoogleEventStorage.ts | 48 +++++++ .../src/assets/scripts/Application/index.ts | 3 + .../Controls/TechniquesViewController.vue | 7 +- .../Elements/PredictTechniquesTool.vue | 68 ++++++++-- .../src/stores/InferenceEngineStore.ts | 2 + 7 files changed, 246 insertions(+), 12 deletions(-) create mode 100644 src/tie-web-interface/src/assets/scripts/Application/EventRecorder.ts create mode 100644 src/tie-web-interface/src/assets/scripts/Application/EventStorage.ts create mode 100644 src/tie-web-interface/src/assets/scripts/Application/GoogleEventStorage.ts diff --git a/src/tie-web-interface/src/assets/scripts/Application/EventRecorder.ts b/src/tie-web-interface/src/assets/scripts/Application/EventRecorder.ts new file mode 100644 index 0000000..951eefc --- /dev/null +++ b/src/tie-web-interface/src/assets/scripts/Application/EventRecorder.ts @@ -0,0 +1,118 @@ +import { SetViewFilter } from "../PredictionsView/Commands/SetViewFilter"; +import { SetViewLimit } from "../PredictionsView/Commands/SetViewLimit"; +import { SetViewOption } from "../PredictionsView/Commands/SetViewOption"; +import type { EventStorage } from "./EventStorage"; +import type { ControlCommand } from "../PredictionsView"; + +export class EventRecorder { + + /** + * The recorder's event store. + */ + private _storage: EventStorage; + + + /** + * Creates a new {@link EventRecorder}. + * @param storage + * The recorder's event store. + */ + constructor(storage: EventStorage) { + this._storage = storage; + } + + + /** + * Records an "add techniques" event. + * @param method + * The method used to add the techniques. + * @param total + * The total number of techniques added. + */ + public addTechniques(method: string, total: number) { + this._storage.record( + "add_techniques", + { + "method": method, + "total_techniques": total + } + ) + } + + /** + * Records a "technique prediction" event. + * @param techniqueBasis + * The number of techniques provided for the prediction. + * @param backend + * The prediction backend. + * @param time + * The prediction time (in ms). + */ + public makePrediction(provided: number, backend: string, time: number,) { + this._storage.record( + "make_prediction", + { + "total_techniques_provided": provided, + "prediction_backend": backend, + "prediction_time": time + } + ); + } + + /** + * Records an "apply view control" event. + * @param cmd + * The applied control command. + */ + public applyViewControl(cmd: ControlCommand) { + // If view filter... + if (cmd instanceof SetViewFilter) { + this._storage.record( + "apply_filter_control", + { + "control": cmd.control.name, + "filter": cmd.filter ?? "all_filters", + "value": cmd.value + } + ); + return; + } + // If view option... + if (cmd instanceof SetViewOption) { + this._storage.record( + "apply_organization_control", + { + "control": cmd.control.name, + "value": cmd.value + } + ); + return; + } + // If view limit... + if (cmd instanceof SetViewLimit) { + this._storage.record( + "apply_view_limit_control", + { + "control": cmd.control.name, + "value": cmd.value + } + ) + return; + } + } + + /** + * Records a "download file" event. + * @param fileType + * The file's type. + */ + public downloadArtifact(fileType: string) { + this._storage.record( + "download_file", + { + "file_type": fileType + } + ) + } + +} diff --git a/src/tie-web-interface/src/assets/scripts/Application/EventStorage.ts b/src/tie-web-interface/src/assets/scripts/Application/EventStorage.ts new file mode 100644 index 0000000..163ed88 --- /dev/null +++ b/src/tie-web-interface/src/assets/scripts/Application/EventStorage.ts @@ -0,0 +1,12 @@ +export interface EventStorage { + + /** + * Records an event to the event store. + * @param name + * The event's name. + * @param parameters + * The event's parameters. + */ + record(name: string, parameters: { [key: string]: string | number | boolean; }): void; + +} diff --git a/src/tie-web-interface/src/assets/scripts/Application/GoogleEventStorage.ts b/src/tie-web-interface/src/assets/scripts/Application/GoogleEventStorage.ts new file mode 100644 index 0000000..938fa4d --- /dev/null +++ b/src/tie-web-interface/src/assets/scripts/Application/GoogleEventStorage.ts @@ -0,0 +1,48 @@ +import type { EventStorage } from "./EventStorage"; + +declare const gtag: ( + command: "event", + event_name: string, + parameters: { [key: string]: string | number | boolean; } +) => void + +export class GoogleEventStorage implements EventStorage { + + /** + * Creates a new {@link GoogleEventStorage}. + */ + constructor() { } + + /** + * Records an event to the event store. + * @remarks + * Please ensure the event `name` follows Google Analytic's naming convention of + * `[verb]_[noun]` where the verb is in the simple present tense. For example: + * - `join_group` + * - `view_item` + * - `select_item` + * - `spend_virtual_currency` + * @param name + * The event's name. + * @param parameters + * The event's parameters. + */ + record(name: string, parameters: { [key: string]: string | number | boolean; }): void { + // Validate record + if (!name.match(/^[a-z][a-z_]*$/g)) { + const error = `Event name '${name}' does not follow typical convention.`; + throw new Error(error); + } + for (const key in parameters) { + const type = typeof parameters[key]; + if (type !== "string" && type !== "number" && type !== "boolean") { + const types = "string, number, or boolean"; + const error = `Value of event parameter '${key}' must be a ${types}.` + throw new Error(error); + } + } + // Record event + gtag('event', name, parameters); + } + +} diff --git a/src/tie-web-interface/src/assets/scripts/Application/index.ts b/src/tie-web-interface/src/assets/scripts/Application/index.ts index 3e4c762..b3a3221 100644 --- a/src/tie-web-interface/src/assets/scripts/Application/index.ts +++ b/src/tie-web-interface/src/assets/scripts/Application/index.ts @@ -1 +1,4 @@ export * from "./AppConfiguration"; +export * from "./EventRecorder"; +export * from "./EventStorage"; +export * from "./GoogleEventStorage"; diff --git a/src/tie-web-interface/src/components/Controls/TechniquesViewController.vue b/src/tie-web-interface/src/components/Controls/TechniquesViewController.vue index 63bcec5..a05a298 100644 --- a/src/tie-web-interface/src/components/Controls/TechniquesViewController.vue +++ b/src/tie-web-interface/src/components/Controls/TechniquesViewController.vue @@ -52,7 +52,6 @@ diff --git a/src/tie-web-interface/src/assets/scripts/Application/EventRecorder.ts b/src/tie-web-interface/src/assets/scripts/Application/EventRecorder.ts index 3408da3..73ddd3c 100644 --- a/src/tie-web-interface/src/assets/scripts/Application/EventRecorder.ts +++ b/src/tie-web-interface/src/assets/scripts/Application/EventRecorder.ts @@ -26,15 +26,21 @@ export class EventRecorder { * Records an "add techniques" event. * @param method * The method used to add the techniques. - * @param total - * The total number of techniques added. + * @param techniques + * The techniques added. */ - public addTechniques(method: string, total: number) { + public addTechniques(method: string, techniques: string[]) { + // Sanitize techniques + const sanitizedTechniques = techniques + .filter(id => id.match(/^T[0-9]{4}(?:.[0-9]{3})?$/gi)) + .map(id => id.toLocaleUpperCase()); + // Record this._storage.record( "add_techniques", { "method": method, - "total_techniques": total + "techniques": sanitizedTechniques, + "total_techniques": sanitizedTechniques.length } ) } @@ -42,17 +48,18 @@ export class EventRecorder { /** * Records a "technique prediction" event. * @param techniqueBasis - * The number of techniques provided for the prediction. + * The observed techniques provided for the prediction. * @param backend * The prediction backend. * @param time * The prediction time (in ms). */ - public makePrediction(provided: number, backend: string, time: number,) { + public makePrediction(techniques: string[], backend: string, time: number,) { this._storage.record( "make_prediction", { - "total_observed_techniques": provided, + "observed_techniques": techniques, + "total_observed_techniques": techniques.length, "prediction_backend": backend, "prediction_time": time } diff --git a/src/tie-web-interface/src/assets/scripts/Application/EventStorage.ts b/src/tie-web-interface/src/assets/scripts/Application/EventStorage.ts index 163ed88..55a3e0a 100644 --- a/src/tie-web-interface/src/assets/scripts/Application/EventStorage.ts +++ b/src/tie-web-interface/src/assets/scripts/Application/EventStorage.ts @@ -1,3 +1,5 @@ +import type { RecordParameter } from "./RecordParameter"; + export interface EventStorage { /** @@ -7,6 +9,6 @@ export interface EventStorage { * @param parameters * The event's parameters. */ - record(name: string, parameters: { [key: string]: string | number | boolean; }): void; + record(name: string, parameters: { [key: string]: RecordParameter; }): void; } diff --git a/src/tie-web-interface/src/assets/scripts/Application/GoogleEventStorage.ts b/src/tie-web-interface/src/assets/scripts/Application/GoogleEventStorage.ts index 938fa4d..63060ba 100644 --- a/src/tie-web-interface/src/assets/scripts/Application/GoogleEventStorage.ts +++ b/src/tie-web-interface/src/assets/scripts/Application/GoogleEventStorage.ts @@ -1,9 +1,10 @@ import type { EventStorage } from "./EventStorage"; +import type { RecordParameter } from "./RecordParameter"; declare const gtag: ( command: "event", event_name: string, - parameters: { [key: string]: string | number | boolean; } + parameters: { [key: string]: RecordParameter; } ) => void export class GoogleEventStorage implements EventStorage { @@ -27,20 +28,12 @@ export class GoogleEventStorage implements EventStorage { * @param parameters * The event's parameters. */ - record(name: string, parameters: { [key: string]: string | number | boolean; }): void { + record(name: string, parameters: { [key: string]: RecordParameter; }): void { // Validate record if (!name.match(/^[a-z][a-z_]*$/g)) { const error = `Event name '${name}' does not follow typical convention.`; throw new Error(error); } - for (const key in parameters) { - const type = typeof parameters[key]; - if (type !== "string" && type !== "number" && type !== "boolean") { - const types = "string, number, or boolean"; - const error = `Value of event parameter '${key}' must be a ${types}.` - throw new Error(error); - } - } // Record event gtag('event', name, parameters); } diff --git a/src/tie-web-interface/src/assets/scripts/Application/RecordParameter.ts b/src/tie-web-interface/src/assets/scripts/Application/RecordParameter.ts new file mode 100644 index 0000000..e2657cf --- /dev/null +++ b/src/tie-web-interface/src/assets/scripts/Application/RecordParameter.ts @@ -0,0 +1 @@ +export type RecordParameter = string | number | boolean | string[] | number[] | boolean[]; diff --git a/src/tie-web-interface/src/assets/scripts/Application/index.ts b/src/tie-web-interface/src/assets/scripts/Application/index.ts index b3a3221..0deb215 100644 --- a/src/tie-web-interface/src/assets/scripts/Application/index.ts +++ b/src/tie-web-interface/src/assets/scripts/Application/index.ts @@ -2,3 +2,4 @@ export * from "./AppConfiguration"; export * from "./EventRecorder"; export * from "./EventStorage"; export * from "./GoogleEventStorage"; +export * from "./RecordParameter"; diff --git a/src/tie-web-interface/src/components/Elements/PredictTechniquesTool.vue b/src/tie-web-interface/src/components/Elements/PredictTechniquesTool.vue index 75c0f23..3fe6e85 100644 --- a/src/tie-web-interface/src/components/Elements/PredictTechniquesTool.vue +++ b/src/tie-web-interface/src/components/Elements/PredictTechniquesTool.vue @@ -179,7 +179,7 @@ export default defineComponent({ async addObservedTechniqueFromPivot(id: string) { // Add Technique this.addObservedTechnique(id); - this.engine.recorder.addTechniques("technique_pivot", 1); + this.engine.recorder.addTechniques("technique_pivot", [id]); // Update predictions await this.updatePredictions(); }, @@ -192,7 +192,7 @@ export default defineComponent({ async addObservedTechniqueFromField(id: string) { // Add Technique this.addObservedTechnique(id); - this.engine.recorder.addTechniques("technique_field", 1); + this.engine.recorder.addTechniques("technique_field", [id]); // Update predictions await this.updatePredictions(); }, @@ -212,7 +212,7 @@ export default defineComponent({ for (let obj of objects) { this.addObservedTechnique(obj.id); } - this.engine.recorder.addTechniques("import_csv", objects.length); + this.engine.recorder.addTechniques("import_csv", objects.map(o => o.id)); // Update predictions await this.updatePredictions(); }, @@ -239,7 +239,7 @@ export default defineComponent({ const techniques = new Set(a.filter(t => b.has(t))); this.predicted = await this.engine.predictNewReport(techniques); this.engine.recorder.makePrediction( - techniques.size, + [...techniques], this.predicted.metadata.humanReadableBackend, this.predicted.metadata.time );