mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(stats): record android as a reading source
Accept android as a reading-session source from clients and report it as its own statistics bucket, labelled Android app, beside the iOS app and Apple Watch. A generated migration adds it to the reading_sessions check constraint, achievements count Android reading toward Full Orbit and Two Worlds, and the web charts draw it in a new --pill-android token at hue 95.
This commit is contained in:
@@ -16,9 +16,10 @@ describe("toReadingSessionSourceBucket", () => {
|
||||
expect(toReadingSessionSourceBucket("manual")).toBe("bookorbit");
|
||||
});
|
||||
|
||||
it("keeps iOS and Apple Watch in distinct buckets", () => {
|
||||
it("keeps iOS, Apple Watch and Android in distinct buckets", () => {
|
||||
expect(toReadingSessionSourceBucket("ios")).toBe("ios");
|
||||
expect(toReadingSessionSourceBucket("watchos")).toBe("watchos");
|
||||
expect(toReadingSessionSourceBucket("android")).toBe("android");
|
||||
});
|
||||
|
||||
it("maps koreader to koreader", () => {
|
||||
@@ -36,8 +37,8 @@ describe("toReadingSessionSourceBucket", () => {
|
||||
});
|
||||
|
||||
describe("reading session source bucket constants", () => {
|
||||
it("exposes all five display buckets", () => {
|
||||
expect(READING_SESSION_SOURCE_BUCKETS).toEqual(["bookorbit", "ios", "watchos", "koreader", "kobo"]);
|
||||
it("exposes all six display buckets", () => {
|
||||
expect(READING_SESSION_SOURCE_BUCKETS).toEqual(["bookorbit", "ios", "watchos", "android", "koreader", "kobo"]);
|
||||
});
|
||||
|
||||
it("labels every bucket", () => {
|
||||
@@ -45,12 +46,13 @@ describe("reading session source bucket constants", () => {
|
||||
bookorbit: "BookOrbit",
|
||||
ios: "iOS app",
|
||||
watchos: "Apple Watch",
|
||||
android: "Android app",
|
||||
koreader: "KOReader",
|
||||
kobo: "Kobo",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds a zero-filled record", () => {
|
||||
expect(emptySourceBucketRecord()).toEqual({ bookorbit: 0, ios: 0, watchos: 0, koreader: 0, kobo: 0 });
|
||||
expect(emptySourceBucketRecord()).toEqual({ bookorbit: 0, ios: 0, watchos: 0, android: 0, koreader: 0, kobo: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import type { ReadingSessionSource } from "./reading-session";
|
||||
|
||||
export const READING_SESSION_SOURCE_BUCKETS = ["bookorbit", "ios", "watchos", "koreader", "kobo"] as const;
|
||||
export const READING_SESSION_SOURCE_BUCKETS = ["bookorbit", "ios", "watchos", "android", "koreader", "kobo"] as const;
|
||||
export type ReadingSessionSourceBucket = (typeof READING_SESSION_SOURCE_BUCKETS)[number];
|
||||
|
||||
export const READING_SESSION_SOURCE_BUCKET_LABELS: Record<ReadingSessionSourceBucket, string> = {
|
||||
bookorbit: "BookOrbit",
|
||||
ios: "iOS app",
|
||||
watchos: "Apple Watch",
|
||||
android: "Android app",
|
||||
koreader: "KOReader",
|
||||
kobo: "Kobo",
|
||||
};
|
||||
|
||||
// Web/manual and null/unknown history collapse into the general BookOrbit bucket.
|
||||
// Native Apple clients remain distinct so device attribution survives aggregation.
|
||||
// Native clients remain distinct so device attribution survives aggregation.
|
||||
export function toReadingSessionSourceBucket(source: ReadingSessionSource | null | undefined): ReadingSessionSourceBucket {
|
||||
if (source === "ios") return "ios";
|
||||
if (source === "watchos") return "watchos";
|
||||
if (source === "android") return "android";
|
||||
if (source === "koreader") return "koreader";
|
||||
if (source === "kobo") return "kobo";
|
||||
return "bookorbit";
|
||||
}
|
||||
|
||||
export function emptySourceBucketRecord(): Record<ReadingSessionSourceBucket, number> {
|
||||
return { bookorbit: 0, ios: 0, watchos: 0, koreader: 0, kobo: 0 };
|
||||
return { bookorbit: 0, ios: 0, watchos: 0, android: 0, koreader: 0, kobo: 0 };
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ReadingSessionSourceBucket } from "./reading-session-source-bucket";
|
||||
|
||||
export const READING_SESSION_SOURCES = ["web", "ios", "watchos", "koreader", "manual", "kobo"] as const;
|
||||
export const READING_SESSION_SOURCES = ["web", "ios", "watchos", "android", "koreader", "manual", "kobo"] as const;
|
||||
export type ReadingSessionSource = (typeof READING_SESSION_SOURCES)[number];
|
||||
|
||||
export const CLIENT_READING_SESSION_SOURCES = ["ios", "watchos"] as const;
|
||||
export const CLIENT_READING_SESSION_SOURCES = ["ios", "watchos", "android"] as const;
|
||||
export type ClientReadingSessionSource = (typeof CLIENT_READING_SESSION_SOURCES)[number];
|
||||
|
||||
export interface BookReadingSession {
|
||||
|
||||
Reference in New Issue
Block a user