From 58fc17e3b6cf71fb0476d849de0440518b93b1cd Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 15 Mar 2023 17:43:13 +0900
Subject: [PATCH] fix: tweak retention rate aggregation

---
 CHANGELOG.md                                  |  1 +
 .../1678869617549-retention-date-key.js       | 14 +++++++++++
 .../models/entities/RetentionAggregation.ts   |  6 +++++
 .../AggregateRetentionProcessorService.ts     | 23 +++++++++++++------
 .../src/components/MkRetentionHeatmap.vue     | 11 +++++----
 5 files changed, 43 insertions(+), 12 deletions(-)
 create mode 100644 packages/backend/migration/1678869617549-retention-date-key.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8686693397..27d72388ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,7 @@ You should also include the user name that made the change.
 - fix(frontend): Safariでプラグインが複数ある場合に正常に読み込まれない問題を修正
 - Bookwyrmのユーザーのプロフィールページで「リモートで表示」をタップしても反応がない問題を修正
 - `disableCache: true`を設定している場合に絵文字管理操作でエラーが出る問題を修正
+- リテンション分析が上手く機能しないことがあるのを修正
 
 ## 13.9.2 (2023/03/06)
 
diff --git a/packages/backend/migration/1678869617549-retention-date-key.js b/packages/backend/migration/1678869617549-retention-date-key.js
new file mode 100644
index 0000000000..1a31b9a750
--- /dev/null
+++ b/packages/backend/migration/1678869617549-retention-date-key.js
@@ -0,0 +1,14 @@
+export class retentionDateKey1678869617549 {
+    name = 'retentionDateKey1678869617549'
+
+    async up(queryRunner) {
+			await queryRunner.query(`TRUNCATE TABLE "retention_aggregation"`, undefined);
+        await queryRunner.query(`ALTER TABLE "retention_aggregation" ADD "dateKey" character varying(512) NOT NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f7c3576b37bd2eec966ae24477" ON "retention_aggregation" ("dateKey") `);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "public"."IDX_f7c3576b37bd2eec966ae24477"`);
+        await queryRunner.query(`ALTER TABLE "retention_aggregation" DROP COLUMN "dateKey"`);
+    }
+}
diff --git a/packages/backend/src/models/entities/RetentionAggregation.ts b/packages/backend/src/models/entities/RetentionAggregation.ts
index c79b762d71..c7bf38b3af 100644
--- a/packages/backend/src/models/entities/RetentionAggregation.ts
+++ b/packages/backend/src/models/entities/RetentionAggregation.ts
@@ -18,6 +18,12 @@ export class RetentionAggregation {
 	})
 	public updatedAt: Date;
 
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 512, nullable: false,
+	})
+	public dateKey: string;
+
 	@Column({
 		...id(),
 		array: true,
diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts
index 02324c6cd4..fcfba75909 100644
--- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts
+++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts
@@ -7,6 +7,7 @@ import { bindThis } from '@/decorators.js';
 import type { RetentionAggregationsRepository, UsersRepository } from '@/models/index.js';
 import { deepClone } from '@/misc/clone.js';
 import { IdService } from '@/core/IdService.js';
+import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type Bull from 'bull';
 
@@ -49,13 +50,21 @@ export class AggregateRetentionProcessorService {
 		});
 		const targetUserIds = targetUsers.map(u => u.id);
 
-		await this.retentionAggregationsRepository.insert({
-			id: this.idService.genId(),
-			createdAt: now,
-			updatedAt: now,
-			userIds: targetUserIds,
-			usersCount: targetUserIds.length,
-		});
+		try {
+			await this.retentionAggregationsRepository.insert({
+				id: this.idService.genId(),
+				createdAt: now,
+				updatedAt: now,
+				dateKey,
+				userIds: targetUserIds,
+				usersCount: targetUserIds.length,
+			});
+		} catch (err) {
+			if (isDuplicateKeyValueError(err)) {
+				this.logger.succ('Skip because it has already been processed by another worker.');
+				done();
+			}
+		}
 
 		// 今日活動したユーザーを全て取得
 		const activeUsers = await this.usersRepository.findBy({
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index 8326ec7ef3..85c009f746 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -36,9 +36,11 @@ async function renderChart() {
 	const wide = rootEl.offsetWidth > 600;
 	const narrow = rootEl.offsetWidth < 400;
 
-	const maxDays = wide ? 15 : narrow ? 5 : 10;
+	const maxDays = wide ? 10 : narrow ? 5 : 7;
 
-	const raw = await os.api('retention', { });
+	let raw = await os.api('retention', { });
+
+	raw = raw.slice(0, maxDays);
 
 	const data = [];
 	for (const record of raw) {
@@ -60,10 +62,9 @@ async function renderChart() {
 	const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
 
 	// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
-	//const max = raw.readWrite.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
-	const max = 4;
+	const max = raw.map(x => x.users).slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
 
-	const marginEachCell = 6;
+	const marginEachCell = 12;
 
 	chartInstance = new Chart(chartEl, {
 		type: 'matrix',