fix(kobo): preserve reading session duration without subtracting idle time

Kobo devices report active reading duration in SecondsRead. Subtracting IdleTime reduced session durations incorrectly, preventing reading logs from populating accurately.

Closes #653
This commit is contained in:
neon
2026-07-13 05:59:14 +00:00
parent 45e0bed6e3
commit 353421ded0
2 changed files with 24 additions and 19 deletions
@@ -112,7 +112,7 @@ describe('KoboAnalyticsService', () => {
expect(readingSessionService.save).toHaveBeenCalledWith(100, expect.objectContaining({ sessionId: 'kobo-src' }), user, 'kobo');
});
it('pairs OpenContent and LeaveContent to save progress delta and active duration', async () => {
it('pairs OpenContent and LeaveContent to save progress delta and Kobo-reported duration', async () => {
await makeService().ingest(
{
Events: [
@@ -158,7 +158,7 @@ describe('KoboAnalyticsService', () => {
sessionId: 'leave-forward',
startedAt: '2026-06-06T18:20:01.000Z',
endedAt: '2026-06-06T18:20:52.000Z',
durationSeconds: 49,
durationSeconds: 52,
progressDelta: 4,
endProgress: 12,
},
@@ -172,7 +172,7 @@ describe('KoboAnalyticsService', () => {
sessionId: 'leave-backward',
startedAt: '2026-06-06T18:21:18.000Z',
endedAt: '2026-06-06T18:21:44.000Z',
durationSeconds: 24,
durationSeconds: 26,
progressDelta: -2,
endProgress: 10,
},
@@ -264,16 +264,23 @@ describe('KoboAnalyticsService', () => {
);
});
it('uses raw SecondsRead when IdleTime is invalid', async () => {
it('preserves SecondsRead when Kobo also reports IdleTime', async () => {
await makeService().ingest(
{
Events: [
{
Id: 'invalid-idle',
Id: 'open-with-idle',
EventType: 'OpenContent',
Timestamp: '2026-06-05T23:19:38Z',
Metrics: {},
Attributes: { volumeid: '1', progress: '0' },
},
{
Id: 'leave-with-idle',
EventType: 'LeaveContent',
Timestamp: '2026-06-01T00:00:20Z',
Metrics: { SecondsRead: 20, IdleTime: '5' as never },
Attributes: { volumeid: '1', progress: '20' },
Timestamp: '2026-06-05T23:20:13Z',
Metrics: { SecondsRead: 35, IdleTime: 28, PagesTurned: 6 },
Attributes: { volumeid: '1', progress: '1' },
},
],
},
@@ -283,11 +290,14 @@ describe('KoboAnalyticsService', () => {
expect(readingSessionService.save).toHaveBeenCalledWith(
100,
expect.objectContaining({
sessionId: 'invalid-idle',
startedAt: '2026-06-01T00:00:00.000Z',
durationSeconds: 20,
}),
{
sessionId: 'leave-with-idle',
startedAt: '2026-06-05T23:19:38.000Z',
endedAt: '2026-06-05T23:20:13.000Z',
durationSeconds: 35,
progressDelta: 1,
endProgress: 1,
},
user,
'kobo',
);
@@ -38,12 +38,7 @@ function parseKoboStars(metrics: KoboAnalyticsEvent['Metrics'] | undefined): Kob
function parseKoboDurationSeconds(metrics: KoboAnalyticsEvent['Metrics'] | undefined): number | null {
const secondsRead = metrics?.SecondsRead;
if (typeof secondsRead !== 'number' || !Number.isFinite(secondsRead) || secondsRead < 0) return null;
const idleTime = metrics?.IdleTime;
const activeSeconds =
typeof idleTime === 'number' && Number.isFinite(idleTime) && idleTime >= 0 ? Math.max(0, secondsRead - idleTime) : secondsRead;
return Math.floor(activeSeconds);
return Math.floor(secondsRead);
}
function formatAnalyticsPayload(value: unknown, maxLength = DEBUG_PAYLOAD_MAX_LENGTH): string {