diff --git a/src/tie-web-interface/index.html b/src/tie-web-interface/index.html index fe4915f..430238d 100644 --- a/src/tie-web-interface/index.html +++ b/src/tie-web-interface/index.html @@ -11,6 +11,14 @@ + + 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..73ddd3c --- /dev/null +++ b/src/tie-web-interface/src/assets/scripts/Application/EventRecorder.ts @@ -0,0 +1,125 @@ +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 techniques + * The techniques added. + */ + 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, + "techniques": sanitizedTechniques, + "total_techniques": sanitizedTechniques.length + } + ) + } + + /** + * Records a "technique prediction" event. + * @param techniqueBasis + * The observed techniques provided for the prediction. + * @param backend + * The prediction backend. + * @param time + * The prediction time (in ms). + */ + public makePrediction(techniques: string[], backend: string, time: number,) { + this._storage.record( + "make_prediction", + { + "observed_techniques": techniques, + "total_observed_techniques": techniques.length, + "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..55a3e0a --- /dev/null +++ b/src/tie-web-interface/src/assets/scripts/Application/EventStorage.ts @@ -0,0 +1,14 @@ +import type { RecordParameter } from "./RecordParameter"; + +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]: 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 new file mode 100644 index 0000000..63060ba --- /dev/null +++ b/src/tie-web-interface/src/assets/scripts/Application/GoogleEventStorage.ts @@ -0,0 +1,41 @@ +import type { EventStorage } from "./EventStorage"; +import type { RecordParameter } from "./RecordParameter"; + +declare const gtag: ( + command: "event", + event_name: string, + parameters: { [key: string]: RecordParameter; } +) => 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]: 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); + } + // 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 3e4c762..0deb215 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,5 @@ export * from "./AppConfiguration"; +export * from "./EventRecorder"; +export * from "./EventStorage"; +export * from "./GoogleEventStorage"; +export * from "./RecordParameter"; diff --git a/src/tie-web-interface/src/assets/styles/engenuity_scaling_system.scss b/src/tie-web-interface/src/assets/styles/engenuity_scaling_system.scss index b2d79f6..5f05f32 100644 --- a/src/tie-web-interface/src/assets/styles/engenuity_scaling_system.scss +++ b/src/tie-web-interface/src/assets/styles/engenuity_scaling_system.scss @@ -676,3 +676,12 @@ input { border-style: solid; border-width: 1px; } + +@mixin selection-area { + & { + transition: background .15s; + } + &:hover { + background: var(--accent-2-border); + } +} diff --git a/src/tie-web-interface/src/components/Controls/TechniqueItemSummary.vue b/src/tie-web-interface/src/components/Controls/TechniqueItemSummary.vue index 9710999..edeb621 100644 --- a/src/tie-web-interface/src/components/Controls/TechniqueItemSummary.vue +++ b/src/tie-web-interface/src/components/Controls/TechniqueItemSummary.vue @@ -110,6 +110,7 @@ svg:not(.collapsed) { } .technique-header { + @include scale.selection-area; display: flex; align-items: stretch; } 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 @@