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

unit test latest metrics

This commit is contained in:
✨ Audrey ✨
2025-03-19 11:32:40 -04:00
parent 723c2f7767
commit 02dbf172f5
4 changed files with 159 additions and 47 deletions

View File

@@ -0,0 +1,9 @@
describe("AchievementHub", () => {
describe("earned$", () => {});
describe("metrics$", () => {});
describe("all$", () => {});
describe("named$", () => {});
});

View File

@@ -1,7 +1,75 @@
import { UserId } from "../../../types/guid";
import { AchievementProgressEvent } from "../types";
import { AchievementEarnedEvent, AchievementProgressEvent } from "../types";
import { CredentialGeneratedProgress, ItemCreatedProgress } from "./example-validators";
import {
CredentialGeneratedProgress,
ItemCreatedAchievement,
ItemCreatedProgress,
ThreeItemsCreatedAchievement,
} from "./example-validators";
const ItemCreatedEarnedEvent: AchievementEarnedEvent = {
"@timestamp": Date.now(),
event: {
kind: "metric",
category: "session",
},
achievement: { type: "earned", name: ItemCreatedAchievement },
service: {
name: "extension",
type: "client",
node: {
name: "an-installation-identifier-for-this-client-instance",
},
environment: "local",
version: "2025.3.1-innovation-sprint",
},
user: {
id: "some-guid" as UserId,
},
};
const NextItemCreatedEarnedEvent: AchievementEarnedEvent = {
"@timestamp": Date.now() + 100,
event: {
kind: "metric",
category: "session",
},
achievement: { type: "earned", name: ItemCreatedAchievement },
service: {
name: "extension",
type: "client",
node: {
name: "an-installation-identifier-for-this-client-instance",
},
environment: "local",
version: "2025.3.1-innovation-sprint",
},
user: {
id: "some-guid" as UserId,
},
};
const ThreeItemsCreatedEarnedEvent: AchievementEarnedEvent = {
"@timestamp": Date.now(),
event: {
kind: "metric",
category: "session",
},
achievement: { type: "earned", name: ThreeItemsCreatedAchievement },
service: {
name: "extension",
type: "client",
node: {
name: "an-installation-identifier-for-this-client-instance",
},
environment: "local",
version: "2025.3.1-innovation-sprint",
},
user: {
id: "some-guid" as UserId,
},
};
const ItemCreatedProgressEvent: AchievementProgressEvent = {
"@timestamp": Date.now(),
@@ -67,7 +135,10 @@ const CredentialGeneratedProgressEvent: AchievementProgressEvent = {
};
export {
ItemCreatedProgressEvent,
NextItemCreatedProgressEvent as ItemCreatedProgress2Event,
CredentialGeneratedProgressEvent,
ItemCreatedEarnedEvent,
ItemCreatedProgressEvent,
NextItemCreatedEarnedEvent,
NextItemCreatedProgressEvent,
ThreeItemsCreatedEarnedEvent,
};

View File

@@ -3,13 +3,21 @@ import { BehaviorSubject, Subject } from "rxjs";
import {
CredentialGeneratedProgressEvent,
ItemCreatedProgressEvent,
ItemCreatedProgress2Event,
NextItemCreatedProgressEvent,
ItemCreatedEarnedEvent,
NextItemCreatedEarnedEvent,
ThreeItemsCreatedEarnedEvent,
} from "./examples/achievement-events";
import { CredentialGeneratedProgress, ItemCreatedProgress } from "./examples/example-validators";
import { latestProgressMetrics } from "./latest-metrics";
import { AchievementProgressEvent, MetricId } from "./types";
import {
CredentialGeneratedProgress,
ItemCreatedAchievement,
ItemCreatedProgress,
ThreeItemsCreatedAchievement,
} from "./examples/example-validators";
import { latestEarnedMetrics, latestProgressMetrics } from "./latest-metrics";
import { AchievementEarnedEvent, AchievementId, AchievementProgressEvent, MetricId } from "./types";
describe("latestMetrics", () => {
describe("latestProgressMetrics", () => {
it("creates a map containing a metric", () => {
const subject = new Subject<AchievementProgressEvent>();
const result = new BehaviorSubject(new Map<MetricId, AchievementProgressEvent>());
@@ -38,9 +46,9 @@ describe("latestMetrics", () => {
subject.pipe(latestProgressMetrics()).subscribe(result);
subject.next(ItemCreatedProgressEvent);
subject.next(ItemCreatedProgress2Event);
subject.next(NextItemCreatedProgressEvent);
expect(result.value.get(ItemCreatedProgress)).toEqual(ItemCreatedProgress2Event);
expect(result.value.get(ItemCreatedProgress)).toEqual(NextItemCreatedProgressEvent);
});
it("omits old events", () => {
@@ -48,9 +56,55 @@ describe("latestMetrics", () => {
const result = new BehaviorSubject(new Map<MetricId, AchievementProgressEvent>());
subject.pipe(latestProgressMetrics()).subscribe(result);
subject.next(ItemCreatedProgress2Event);
subject.next(NextItemCreatedProgressEvent);
subject.next(ItemCreatedProgressEvent);
expect(result.value.get(ItemCreatedProgress)).toEqual(ItemCreatedProgress2Event);
expect(result.value.get(ItemCreatedProgress)).toEqual(NextItemCreatedProgressEvent);
});
});
describe("latestEarnedMetrics", () => {
it("creates a map containing a metric", () => {
const subject = new Subject<AchievementEarnedEvent>();
const result = new BehaviorSubject(new Map<AchievementId, AchievementEarnedEvent>());
subject.pipe(latestEarnedMetrics()).subscribe(result);
subject.next(ItemCreatedEarnedEvent);
expect(result.value.get(ItemCreatedAchievement)).toEqual(ItemCreatedEarnedEvent);
});
it("creates a map containing multiple metrics", () => {
const subject = new Subject<AchievementEarnedEvent>();
const result = new BehaviorSubject(new Map<AchievementId, AchievementEarnedEvent>());
subject.pipe(latestEarnedMetrics()).subscribe(result);
subject.next(ItemCreatedEarnedEvent);
subject.next(ThreeItemsCreatedEarnedEvent);
expect(result.value.get(ItemCreatedAchievement)).toEqual(ItemCreatedEarnedEvent);
expect(result.value.get(ThreeItemsCreatedAchievement)).toEqual(ThreeItemsCreatedEarnedEvent);
});
it("creates a map containing updated metrics", () => {
const subject = new Subject<AchievementEarnedEvent>();
const result = new BehaviorSubject(new Map<AchievementId, AchievementEarnedEvent>());
subject.pipe(latestEarnedMetrics()).subscribe(result);
subject.next(ItemCreatedEarnedEvent);
subject.next(NextItemCreatedEarnedEvent);
expect(result.value.get(ItemCreatedAchievement)).toEqual(NextItemCreatedEarnedEvent);
});
it("omits old events", () => {
const subject = new Subject<AchievementEarnedEvent>();
const result = new BehaviorSubject(new Map<AchievementId, AchievementEarnedEvent>());
subject.pipe(latestEarnedMetrics()).subscribe(result);
subject.next(NextItemCreatedEarnedEvent);
subject.next(ItemCreatedEarnedEvent);
expect(result.value.get(ItemCreatedAchievement)).toEqual(NextItemCreatedEarnedEvent);
});
});

View File

@@ -1,36 +1,7 @@
import { OperatorFunction, map, filter, pipe, scan } from "rxjs";
import { OperatorFunction, pipe, scan } from "rxjs";
import { MetricId, AchievementProgressEvent, AchievementId, AchievementEarnedEvent } from "./types";
function latestProgressEvents(): OperatorFunction<
AchievementProgressEvent,
AchievementProgressEvent
> {
type Accumulator = {
latest: Map<MetricId, AchievementProgressEvent>;
captured?: AchievementProgressEvent;
};
const acc: Accumulator = { latest: new Map() };
return pipe(
scan((acc, captured) => {
const { latest } = acc;
const current = latest.get(captured.achievement.name);
// omit stale events
if (current && current["@timestamp"] > captured["@timestamp"]) {
return { latest };
}
latest.set(captured.achievement.name, captured);
return { latest, captured };
}, acc),
// omit updates caused by stale events
filter(({ captured }) => !!captured),
map(({ captured }) => captured!),
);
}
function latestProgressMetrics(): OperatorFunction<
AchievementProgressEvent,
Map<MetricId, AchievementProgressEvent>
@@ -56,11 +27,18 @@ function latestEarnedMetrics(): OperatorFunction<
Map<AchievementId, AchievementEarnedEvent>
> {
return pipe(
scan((earned, captured) => {
earned.set(captured.achievement.name, captured);
return earned;
scan((metrics, captured) => {
const metric = metrics.get(captured.achievement.name);
// omit stale metrics
if (metric && metric["@timestamp"] > captured["@timestamp"]) {
return metrics;
}
metrics.set(captured.achievement.name, captured);
return metrics;
}, new Map<AchievementId, AchievementEarnedEvent>()),
);
}
export { latestProgressMetrics, latestProgressEvents, latestEarnedMetrics };
export { latestProgressMetrics, latestEarnedMetrics };