1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

even more cleanup

This commit is contained in:
✨ Audrey ✨
2025-03-20 22:16:30 -04:00
parent bf089705a7
commit 786603559a
16 changed files with 82 additions and 174 deletions

View File

@@ -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<UserActionEvent>();
/**
* 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<AchievementValidator[]>([TotallyAttachedValidator]);
const achievements$ = new Subject<AchievementEvent>();
const hub = new AchievementHub(validators$, events$, achievements$);
return hub;
},
deps: [
],
}),
];
@NgModule({

View File

@@ -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<EventInfo>();
async ngOnInit() {}
addEvent() {
this._addEvent.next({
action: "vault-item-added",

View File

@@ -2,7 +2,7 @@
<bit-container>
<achievements-list></achievements-list>
<button bitButton type="button" bitFormButton buttonType="primary" (click)="testAchievement()">
Test achievements
<button bitButton type="button" bitFormButton buttonType="primary" (click)="addEvent()">
Send synthetic add vault item event
</button>
</bit-container>

View File

@@ -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<EventInfo>();
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"],
});
}
}

View File

@@ -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({

View File

@@ -3,13 +3,13 @@
<h2 class="tw-font-bold" bitTypography="h6">
{{ "achievements" | i18n }}
</h2>
<span bitTypography="body1" slot="end">{{ allAchievementCards.length }}</span>
<span bitTypography="body1" slot="end">{{ achievements.length }}</span>
</bit-section-header>
<bit-item-group>
@for (achievement of achievements; track achievement.name) {
<achievement-item
[icon]="lookupIcon(achievement)"
[icon]="icon(achievement)"
[title]="achievement.name"
[description]="achievement.description"
[earned]="isEarned(achievement)"

View File

@@ -1,5 +1,5 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Component, NgZone } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { filter, switchMap } from "rxjs";
@@ -51,6 +51,7 @@ export class AchievementsListComponent {
constructor(
private achievementService: AchievementService,
private accountService: AccountService,
zone: NgZone,
) {
this.achievements = achievementService.achievementMap();
@@ -60,7 +61,7 @@ export class AchievementsListComponent {
switchMap((account) => 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;
}
}

View File

@@ -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(),

View File

@@ -97,9 +97,4 @@ export class AchievementHub {
startWith(new Map<MetricId, AchievementProgressEvent>()),
);
}
//Test methods
addEvent(event: AchievementEvent) {
this.achievementLog.next(event);
}
}

View File

@@ -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<string, AchievementHub>();

View File

@@ -1,10 +0,0 @@
import { Observable } from "rxjs";
import { AchievementEarnedEvent, AchievementProgressEvent, UserActionEvent } from "./types";
export abstract class EventStoreAbstraction {
abstract events$: Observable<UserActionEvent | AchievementProgressEvent | AchievementEarnedEvent>;
abstract addEvent(
event: UserActionEvent | AchievementProgressEvent | AchievementEarnedEvent,
): boolean;
}

View File

@@ -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<AchievementProgressEvent | AchievementEarnedEvent>();
events$: Observable<AchievementProgressEvent | AchievementEarnedEvent> =
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;
}
}

View File

@@ -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<AchievementEvent>();
// data incoming from the UI (consumed by validator)
const userActionIn$ = new Subject<UserActionEvent>();
// what to look for (consumed by validator)
const achievementMonitors$ = new Subject<AchievementValidator[]>();
// data stored in local state (consumed by validator and achievement list)
const achievementsLocal$ = new Subject<AchievementEvent[]>();
// metadata (consumed by achievement list)
const achievementMetadata$ = new Subject<Achievement>();
export {
replicationIn$,
userActionIn$,
achievementsLocal$,
achievementMonitors$,
achievementMetadata$,
};

View File

@@ -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<MetricId, number>) => 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<MetricId, number>) => 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<MetricId, number>,
) => AchievementEarnedEvent[];
};

View File

@@ -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);
});
});

View File

@@ -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),
);
}