From 786603559a40fa5ad1e599a3c94be2ee4fae397f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 20 Mar 2025 22:16:30 -0400 Subject: [PATCH] even more cleanup --- .../src/popup/services/services.module.ts | 19 +----- .../achievements/achievements.component.ts | 13 ++-- .../achievements/achievements.component.html | 4 +- .../achievements/achievements.component.ts | 63 ++++++++----------- .../src/services/jslib-services.module.ts | 4 +- .../achievements-list.component.html | 4 +- .../achievements-list.component.ts | 9 +-- .../tools/achievements/achievement-events.ts | 2 + .../src/tools/achievements/achievement-hub.ts | 5 -- ...vice.ts => default-achievement.service.ts} | 2 +- .../event-store.abstraction.service.ts | 10 --- .../src/tools/achievements/event-store.ts | 22 ------- libs/common/src/tools/achievements/inputs.ts | 26 -------- libs/common/src/tools/achievements/types.ts | 54 ++++++++++------ .../src/tools/achievements/util.spec.ts | 11 ---- libs/common/src/tools/achievements/util.ts | 8 --- 16 files changed, 82 insertions(+), 174 deletions(-) rename libs/common/src/tools/achievements/{next-achievement.service.ts => default-achievement.service.ts} (97%) delete mode 100644 libs/common/src/tools/achievements/event-store.abstraction.service.ts delete mode 100644 libs/common/src/tools/achievements/event-store.ts delete mode 100644 libs/common/src/tools/achievements/inputs.ts delete mode 100644 libs/common/src/tools/achievements/util.spec.ts delete mode 100644 libs/common/src/tools/achievements/util.ts diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 780ad8aba91..86ab11a374a 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { Router } from "@angular/router"; -import { BehaviorSubject, merge, of, Subject } from "rxjs"; +import { merge, of, Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; @@ -111,9 +111,6 @@ import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/imp import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service"; import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { AchievementHub } from "@bitwarden/common/tools/achievements/achievement-hub"; -import { TotallyAttachedValidator, TotallyAttachedAchievement } from "@bitwarden/common/tools/achievements/examples/example-validators"; -import {AchievementEvent, AchievementValidator, UserActionEvent } from "@bitwarden/common/tools/achievements/types"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -195,8 +192,6 @@ const DISK_BACKUP_LOCAL_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService >("DISK_BACKUP_LOCAL_STORAGE"); -const events$ = new Subject(); - /** * Provider definitions used in the ngModule. * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. @@ -667,18 +662,6 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultSshImportPromptService, deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction], }), - safeProvider({ - provide: AchievementHub, - useFactory: ( - ) => { - const validators$ = new BehaviorSubject([TotallyAttachedValidator]); - const achievements$ = new Subject(); - const hub = new AchievementHub(validators$, events$, achievements$); - return hub; - }, - deps: [ - ], - }), ]; @NgModule({ diff --git a/apps/browser/src/tools/popup/achievements/achievements.component.ts b/apps/browser/src/tools/popup/achievements/achievements.component.ts index 2d7d6968669..7326090ada6 100644 --- a/apps/browser/src/tools/popup/achievements/achievements.component.ts +++ b/apps/browser/src/tools/popup/achievements/achievements.component.ts @@ -1,12 +1,13 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Subject, combineLatestWith, filter, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AchievementsListComponent } from "@bitwarden/angular/tools/achievements/achievements-list.component"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { EventInfo, UserEventLogProvider } from "@bitwarden/common/tools/log/logger"; +import { UserEventCollector } from "@bitwarden/common/tools/log/user-event-collector"; +import { EventInfo } from "@bitwarden/common/tools/log/user-event-monitor"; import { ButtonModule, IconModule } from "@bitwarden/components"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; @@ -29,16 +30,16 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co AchievementsListComponent, ], }) -export class AchievementsComponent implements OnInit { +export class AchievementsComponent { constructor( private accountService: AccountService, - private readonly eventLogs: UserEventLogProvider, + private readonly collector: UserEventCollector, ) { // FIXME: add a subscription to this service and feed the data somewhere this.accountService.activeAccount$ .pipe( filter((account): account is Account => !!account), - map((account) => this.eventLogs.capture(account)), + map((account) => this.collector.monitor(account)), combineLatestWith(this._addEvent), takeUntilDestroyed(), ) @@ -47,8 +48,6 @@ export class AchievementsComponent implements OnInit { private _addEvent = new Subject(); - async ngOnInit() {} - addEvent() { this._addEvent.next({ action: "vault-item-added", diff --git a/apps/web/src/app/tools/achievements/achievements.component.html b/apps/web/src/app/tools/achievements/achievements.component.html index ba569c78837..238760a8c79 100644 --- a/apps/web/src/app/tools/achievements/achievements.component.html +++ b/apps/web/src/app/tools/achievements/achievements.component.html @@ -2,7 +2,7 @@ - diff --git a/apps/web/src/app/tools/achievements/achievements.component.ts b/apps/web/src/app/tools/achievements/achievements.component.ts index bb50057c403..7af09c86a80 100644 --- a/apps/web/src/app/tools/achievements/achievements.component.ts +++ b/apps/web/src/app/tools/achievements/achievements.component.ts @@ -1,13 +1,11 @@ -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Subject, combineLatestWith, filter, map } from "rxjs"; -import { AchievementNotifierService } from "@bitwarden/angular/tools/achievements/achievement-notifier.abstraction"; import { AchievementsListComponent } from "@bitwarden/angular/tools/achievements/achievements-list.component"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { EventStoreAbstraction } from "@bitwarden/common/tools/achievements/event-store.abstraction.service"; -import { VaultItems_10_Added_Achievement } from "@bitwarden/common/tools/achievements/examples/achievements"; -import { AchievementEarnedEvent, AchievementId } from "@bitwarden/common/tools/achievements/types"; -import { UserId } from "@bitwarden/common/types/guid"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { UserEventCollector } from "@bitwarden/common/tools/log/user-event-collector"; +import { EventInfo } from "@bitwarden/common/tools/log/user-event-monitor"; import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; @@ -17,40 +15,29 @@ import { SharedModule } from "../../shared"; standalone: true, imports: [SharedModule, HeaderModule, AchievementsListComponent], }) -export class AchievementsComponent implements OnInit { - private currentUserId: UserId; - +export class AchievementsComponent { constructor( - private eventStore: EventStoreAbstraction, - private achievementNotifierService: AchievementNotifierService, private accountService: AccountService, - ) {} - - async ngOnInit() { - await this.achievementNotifierService.init(); - this.currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; + private readonly collector: UserEventCollector, + ) { + // FIXME: add a subscription to this service and feed the data somewhere + this.accountService.activeAccount$ + .pipe( + filter((account): account is Account => !!account), + map((account) => this.collector.monitor(account)), + combineLatestWith(this._addEvent), + takeUntilDestroyed(), + ) + .subscribe(([capture, event]) => capture.info(event)); } - testAchievement() { - const earnedAchievement: AchievementEarnedEvent = { - "@timestamp": Date.now(), - event: { - kind: "alert", - category: "session", - }, - service: { - name: "web", - type: "client", - node: { - name: "an-installation-identifier-for-this-client-instance", - }, - environment: "local", - version: "2025.3.1-innovation-sprint", - }, - user: { id: this.currentUserId }, - achievement: { type: "earned", name: VaultItems_10_Added_Achievement.name as AchievementId }, - }; + private _addEvent = new Subject(); - this.eventStore.addEvent(earnedAchievement); + addEvent() { + this._addEvent.next({ + action: "vault-item-added", + labels: { "vault-item-type": "login", "vault-item-uri-quantity": 1 }, + tags: ["with-attachment"], + }); } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 77a2bdc1ecd..31f2d4060bc 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -248,7 +248,7 @@ import { EventCollectionService } from "@bitwarden/common/services/event/event-c import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { AchievementService } from "@bitwarden/common/tools/achievements/achievement.service.abstraction"; -import { NextAchievementService } from "@bitwarden/common/tools/achievements/next-achievement.service"; +import { DefaultAchievementService } from "@bitwarden/common/tools/achievements/next-achievement.service"; import { DefaultUserEventCollector } from "@bitwarden/common/tools/log/default-user-event-collector"; import { UserEventCollector } from "@bitwarden/common/tools/log/user-event-collector"; import { @@ -1503,7 +1503,7 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: AchievementService, - useClass: NextAchievementService, + useClass: DefaultAchievementService, deps: [UserEventCollector], }), safeProvider({ diff --git a/libs/angular/src/tools/achievements/achievements-list.component.html b/libs/angular/src/tools/achievements/achievements-list.component.html index 1111b191e0b..34253cb1bbc 100644 --- a/libs/angular/src/tools/achievements/achievements-list.component.html +++ b/libs/angular/src/tools/achievements/achievements-list.component.html @@ -3,13 +3,13 @@

{{ "achievements" | i18n }}

- {{ allAchievementCards.length }} + {{ achievements.length }} @for (achievement of achievements; track achievement.name) { this.achievementService.earnedMap$(account)), takeUntilDestroyed(), ) - .subscribe((earned) => (this._earned = earned)); + .subscribe((earned) => zone.run(() => (this._earned = earned))); this.accountService.activeAccount$ .pipe( @@ -68,7 +69,7 @@ export class AchievementsListComponent { switchMap((account) => this.achievementService.metricsMap$(account)), takeUntilDestroyed(), ) - .subscribe((progress) => (this._progress = progress)); + .subscribe((progress) => zone.run(() => (this._progress = progress))); } protected isEarned(achievement: Achievement) { @@ -87,7 +88,7 @@ export class AchievementsListComponent { return this._progress.get(achievement.active.metric)?.achievement?.value ?? -1; } - protected lookupIcon(achievement: Achievement): Icon { + protected icon(achievement: Achievement): Icon { return (iconMap[achievement.achievement] as Icon) ?? AchievementIcon; } } diff --git a/libs/common/src/tools/achievements/achievement-events.ts b/libs/common/src/tools/achievements/achievement-events.ts index 33273ab852d..f5665faf3eb 100644 --- a/libs/common/src/tools/achievements/achievement-events.ts +++ b/libs/common/src/tools/achievements/achievement-events.ts @@ -2,6 +2,7 @@ import { UserId } from "../../types/guid"; import { AchievementEarnedEvent, AchievementId, AchievementProgressEvent, MetricId } from "./types"; +// FIXME: see <./types.ts> AchievementValidator export function progressEvent(name: MetricId, value: number = 1): AchievementProgressEvent { return { "@timestamp": Date.now(), @@ -25,6 +26,7 @@ export function progressEvent(name: MetricId, value: number = 1): AchievementPro }; } +// FIXME: see <./types.ts> AchievementValidator export function earnedEvent(name: AchievementId): AchievementEarnedEvent { return { "@timestamp": Date.now(), diff --git a/libs/common/src/tools/achievements/achievement-hub.ts b/libs/common/src/tools/achievements/achievement-hub.ts index 53e822a2459..c34baa7e113 100644 --- a/libs/common/src/tools/achievements/achievement-hub.ts +++ b/libs/common/src/tools/achievements/achievement-hub.ts @@ -97,9 +97,4 @@ export class AchievementHub { startWith(new Map()), ); } - - //Test methods - addEvent(event: AchievementEvent) { - this.achievementLog.next(event); - } } diff --git a/libs/common/src/tools/achievements/next-achievement.service.ts b/libs/common/src/tools/achievements/default-achievement.service.ts similarity index 97% rename from libs/common/src/tools/achievements/next-achievement.service.ts rename to libs/common/src/tools/achievements/default-achievement.service.ts index dfebe2b4dea..8a4b90f9311 100644 --- a/libs/common/src/tools/achievements/next-achievement.service.ts +++ b/libs/common/src/tools/achievements/default-achievement.service.ts @@ -12,7 +12,7 @@ import { SendItemCreatedCountConfig } from "./validators/config/send-created-cou import { SendItemCreatedCountValidator } from "./validators/send-item-created-count-validator"; import { VaultItemCreatedCountValidator } from "./validators/vault-item-created-count-validator"; -export class NextAchievementService implements AchievementService { +export class DefaultAchievementService implements AchievementService { constructor(private readonly collector: UserEventCollector) {} private hubs = new Map(); diff --git a/libs/common/src/tools/achievements/event-store.abstraction.service.ts b/libs/common/src/tools/achievements/event-store.abstraction.service.ts deleted file mode 100644 index 03252f390f0..00000000000 --- a/libs/common/src/tools/achievements/event-store.abstraction.service.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Observable } from "rxjs"; - -import { AchievementEarnedEvent, AchievementProgressEvent, UserActionEvent } from "./types"; - -export abstract class EventStoreAbstraction { - abstract events$: Observable; - abstract addEvent( - event: UserActionEvent | AchievementProgressEvent | AchievementEarnedEvent, - ): boolean; -} diff --git a/libs/common/src/tools/achievements/event-store.ts b/libs/common/src/tools/achievements/event-store.ts deleted file mode 100644 index 5fcbf7b3a43..00000000000 --- a/libs/common/src/tools/achievements/event-store.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Observable, Subject } from "rxjs"; - -import { EventStoreAbstraction } from "./event-store.abstraction.service"; -import { AchievementEarnedEvent, AchievementProgressEvent } from "./types"; - -// Will be replaced by the achievementHub -export class EventStore implements EventStoreAbstraction { - private _events = new Subject(); - - events$: Observable = - this._events.asObservable(); - - constructor() {} - - addEvent(event: AchievementProgressEvent | AchievementEarnedEvent): boolean { - // FIXME Collapse existing of same metric/higher count AchievementProgressEvents - //eslint-disable-next-line no-console - console.log("EventStore.addEvent", event); - this._events.next(event); - return true; - } -} diff --git a/libs/common/src/tools/achievements/inputs.ts b/libs/common/src/tools/achievements/inputs.ts deleted file mode 100644 index 501fa4aea94..00000000000 --- a/libs/common/src/tools/achievements/inputs.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Subject } from "rxjs"; - -import { Achievement, AchievementEvent, AchievementValidator, UserActionEvent } from "./types"; - -// sync data from the server (consumed by event store) -const replicationIn$ = new Subject(); - -// data incoming from the UI (consumed by validator) -const userActionIn$ = new Subject(); - -// what to look for (consumed by validator) -const achievementMonitors$ = new Subject(); - -// data stored in local state (consumed by validator and achievement list) -const achievementsLocal$ = new Subject(); - -// metadata (consumed by achievement list) -const achievementMetadata$ = new Subject(); - -export { - replicationIn$, - userActionIn$, - achievementsLocal$, - achievementMonitors$, - achievementMetadata$, -}; diff --git a/libs/common/src/tools/achievements/types.ts b/libs/common/src/tools/achievements/types.ts index 14a5c5b8557..97f5815d71f 100644 --- a/libs/common/src/tools/achievements/types.ts +++ b/libs/common/src/tools/achievements/types.ts @@ -21,49 +21,67 @@ export type AchievementEarnedEvent = EventFormat & export type AchievementEvent = AchievementProgressEvent | AchievementEarnedEvent; type MetricCriteria = { - // the metric observed by low/high triggers + /** the metric observed by low/high triggers */ metric: MetricId; } & RequireAtLeastOne<{ - // criteria fail when the metric is less than or equal to `low` + /** criteria fail when the metric is less than or equal to `low` */ low: number; - // criteria fail when the metric is greater than `high` + /** criteria fail when the metric is greater than `high` */ high: number; }>; type ActiveCriteria = "until-earned" | MetricCriteria; -// consumed by validator and achievement list (should this include a "toast-alerter"?) +/** consumed by validator and achievement list (should this include a "toast-alerter"?) */ export type Achievement = { - // identifies the achievement being monitored + /** identifies the achievement being monitored */ achievement: AchievementId; - // human-readable name of the achievement + /** human-readable name of the achievement */ name: string; - // human-readable description of the achievement + /** human-readable description of the achievement */ description?: string; - // conditions that determine when the achievement validator should be loaded - // by the processor + /* conditions that determine when the achievement validator should be loaded + * by the processor + */ active: ActiveCriteria; - // identifies the validator containing filter/measure/earn methods + /** identifies the validator containing filter/measure/earn methods */ validator: ValidatorId; - // whether or not the achievement is hidden until it is earned + /** whether or not the achievement is hidden until it is earned */ hidden: boolean; }; -// consumed by validator +/** An achievement completion monitor */ +// +// FIXME: +// * inject a monitor/capture interface into measure and rewards +// * this interface contains methods from <./achievement-events.ts> +// * it constructs context-specific events, filling in device/time/etc export type AchievementValidator = Achievement & { - // when the watch triggers on incoming user events - trigger: (item: UserActionEvent) => boolean; + /** when the watch triggers on incoming user events + * @param event a monitored user action event + * @returns true when the validator should process the event, otherwise false. + */ + trigger: (event: UserActionEvent) => boolean; - // observe data from the event stream and produces measurements - measure?: (item: UserActionEvent, metrics: Map) => AchievementProgressEvent[]; + /** observes data from the event stream and produces measurements; + * this runs after the event is triggered. + * @param event a monitored user action event + * @returns a collection of measurements extracted from the event (may be empty) + */ + measure?: (event: UserActionEvent, metrics: Map) => AchievementProgressEvent[]; - // monitors achievement progress and emits earned achievements + /** monitors achievement progress and emits earned achievements; + * this runs after all measurements are taken. + * @param progress events emitted by `measure`. If `measure` is undefined, this is an empty array. + * @param metrics last-recorded progress value for all achievement metrics + * @returns a collection of achievements awarded by the event. + */ award?: ( - events: AchievementProgressEvent[], + progress: AchievementProgressEvent[], metrics: Map, ) => AchievementEarnedEvent[]; }; diff --git a/libs/common/src/tools/achievements/util.spec.ts b/libs/common/src/tools/achievements/util.spec.ts deleted file mode 100644 index 8aa5e547394..00000000000 --- a/libs/common/src/tools/achievements/util.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ItemCreatedProgressEvent } from "./examples/achievement-events"; -import { ItemCreatedProgress } from "./examples/example-validators"; -import { mapProgressByName } from "./util"; - -describe("mapProgressByName", () => { - it("creates a map containing a progress value", () => { - const result = mapProgressByName([ItemCreatedProgressEvent]); - - expect(result.get(ItemCreatedProgress)).toEqual(ItemCreatedProgressEvent.achievement.value); - }); -}); diff --git a/libs/common/src/tools/achievements/util.ts b/libs/common/src/tools/achievements/util.ts deleted file mode 100644 index 0a9f364d7fc..00000000000 --- a/libs/common/src/tools/achievements/util.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isProgressEvent } from "./meta"; -import { AchievementEvent } from "./types"; - -export function mapProgressByName(status: AchievementEvent[]) { - return new Map( - status.filter(isProgressEvent).map((e) => [e.achievement.name, e.achievement.value] as const), - ); -}