複雑なユーザーアクティビティレポート要件

Discourse で、毎年任意の 2 つのカスタム日付間の全ユーザー活動を要約したアクティビティレポートを生成し、その結果を Excel 互換ファイルとして保存する方法を探しています(現在の標準機能では、今日の日付との関係でのみ利用可能なプリセット範囲しか提供されていないようです)。

  • メールで送信され、メールで返信された投稿も含まれるようにしたいです(オンラインでフォーラムを利用している際に同じ投稿が閲覧または投稿された場合の二重カウントは避けます)。現在のレポート機能では、メール経由の投稿は除外されているようです。

  • これらの結果を、特定のカスタムユーザープロフィールフィールド(固有の会員番号)に基づいてフィルタリングしたいです。

  • 指定された数値範囲外の会員番号を持つユーザーの結果は除外したいです。

  • 理想的には、期間ごとのユーザーごとのアクティビティポイントスコアを生成でき、閲覧した投稿、投稿した投稿、いいねされた投稿に基づいて重み付けしたいです。

  • 各アクティビティ変数の重み付け(乗数)は、管理者が調整・設定できるようにしたいです。結果は 5 点単位で切り捨てられ、管理者が設定した最大ポイント数で切り捨てられます。

  • できれば、ユーザーごとのトピックカテゴリ/タグの内訳も表示されると嬉しいです。

  • 理想的には、このレポートが毎年指定された時間に自動的に生成され、私にメールで送信されると完璧です。

これらすべては実現可能でしょうか?

これを実現するには新しいカスタムプラグインが必要でしょうか、それとも現在の Data Explorer プラグイン内で何らかの高度な SQL クエリによって可能でしょうか?

あるいは、賢明な選択肢としては、比較的単純な「全データエクスポート」オプションを探し、残りの処理を Excel で行うことでしょうか?

「いいね!」 2

データエクスプローラーがここで最適なアプローチでしょう。タスクごとに異なるクエリを作成する必要がある場合があります(例:ユーザーごとの内訳は別のクエリになります)。

データエクスプローラーで唯一できないのは、「自動生成されてメールで送信される」という点です。これが要件である場合、他のシステムからデータエクスプローラー API を呼び出すことで対応できる可能性があります。

「いいね!」 3

ありがとう、David。
どうやらSQLクエリについて理解を深める必要がありそうです。

あるクエリが、別のクエリからのフィルタリングされた出力を入力として受け取れるでしょうか?また、メールリストに基づくインタラクションは確実に含めることができるのでしょうか?それは何か別の方法で保存されているのでしょうか?標準的なアクティビティレポートから何らかの不明瞭だが克服できない理由で除外されているのではないかと心配していました。

いいえ、クエリのセクションをコピー&ペーストする必要があります。

データベース内の posts テーブルには via_email というブーリアンが含まれているため、メールで作成された投稿を特定することは可能です :+1:

ただし、Discourse は送信するメールにトラッカーを含めていないため、メール通知が「既読」になったかどうかを判断する方法はありません。

組み込みのユーザー活動レポート機能は、どこからでもコピーして自由にいじることができるSQLクエリを通じて処理されていますか?そうすれば、無駄に時間を費やして車輪の再発明をすることにならずに済みます。

「1 日あたりのアクティブユーザー」レポートをご覧になっていますか?これはこのロジックで生成されており、ActiveRecord を使用しています(つまり、生 SQL は使用していません)。それでも、このロジックは有用な出発点となる可能性があります。

ユーザーごとのアクティビティレポート(CSV 形式、指定した日付範囲)を作成したいと考えています。最終的な目的は、オンラインまたはメールで受信・読まれたメッセージ、オンラインまたはメールで投稿されたメッセージ、そして「いいね」がついたメッセージにはより多くのポイントが付与されるという仕組みに基づき、各ユーザーに年間(または他の期間)のアクティビティポイントスコアを付与することです。ベースとして検討しているレポートは、「Admin/Users」をクリックした際に表示される最初のものです。これはすでに私が望む機能の多くを実装しています。

ユーザーディレクトリのロジックは、こちらで確認できます: discourse/app/models/directory_item.rb at 1e66e4602f408d7b2d1105b6c228542dce7f4b38 · discourse/discourse · GitHub

SQLで複製するのは、少なくとも私にとっては簡単な作業には見えないようです。似たようなものを直接コピーできるものがなければ、ゼロから実行できるようになるまでSQLを学ばなければならないからです。私が最後にプログラミングをしたのは、高校時代、BASICがまだあまり恥ずかしいと認められなかった頃のことです。

「いいね!」 1

はい、これを実装するにはそれなりの SQL の知識が必要だと思います。作業に予算があれば、Marketplace で手伝ってくれる人を見つけることができるかもしれません。

ありがとうございます。予算はありません(フォーラムは基本ホスティング費用以上の寄付収入をほとんど得られていません)が、どうやらその方向に進まざるを得なさそうです。

「いいね!」 1

@Paul_King

このクエリが役立つかもしれません。

https://meta.discourse.org/t/daily-weekly-or-total-stats-by-user-over-a-specified-time-range/275167u=grayden_shand

ありがとうございます!試してみたのですが、構文エラーが発生します。

PG::SyntaxError: エラー: "WITH"付近で構文エラーが発生しました
行 13: WITH date_range AS (

(お詫び申し上げます。そのスレッドを通じてあなたにPMを送ってしまいましたが、その後、あなたと元の投稿者が同一人物であると気づきました!)

「いいね!」 1

変数にどのような値を使用されているか伺ってもよろしいでしょうか?

クエリを取得し、以下の値でテストサイトに実行しました:

  • start_date: 2021-07-01
  • end_date: 2021-07-30
  • coverage: all

こんにちは、いくつか試してみましたが、すべて同じ結果になりました。例えば、

です。

申し訳ありませんが、エラーを再現することができませんでした。

ご使用のクエリをここに貼り付けていただけますか?

SELECT 1-- カバレッジ: 'week'、'all'、または 'date'
-- [パラメータ]
-- date :start_date = 2019-08-27
-- date :end_date = 2019-09-30
-- text :coverage = week

WITH date_range AS (
SELECT date_trunc('day', dd):: date AS "date", EXTRACT(week from date_trunc('day', dd):: date) AS "week"
FROM generate_series
        ( :start_date::timestamp 
        , :end_date::timestamp
        , '1 day'::interval) dd
), likes_given AS (
SELECT u.id, dr.date, dr.week, count(pa.*) AS "likes"
FROM date_range dr
FULL JOIN users u ON (1=1)
LEFT JOIN post_actions pa ON (pa.created_at:: date = dr.date and post_action_type_id=2 AND user_id = u.id)
GROUP BY dr.date, dr.week, u.id
ORDER BY u.id, dr.date
), posts_summary AS (SELECT u.id, u.username, u.created_at, dr.*, count(p.id) - count(t.id) AS replies, count(t.id) AS topics, COALESCE(sum(p.like_count),0) AS likes_received
from date_range dr
FULL OUTER JOIN users u ON (1=1)
LEFT JOIN posts p ON (p.user_id = u.id AND p.created_at::date=dr.date AND p.deleted_at IS NULL)
LEFT JOIN topics t ON (t.user_id = u.id AND t.created_at::date = dr.date AND p.topic_id = t.id AND t.deleted_at IS NULL)
GROUP BY u.id, dr.date, dr.week
ORDER BY u.id, dr.date), 
visits AS (SELECT u.id, dr.*, COALESCE(sum(posts_read),0) AS posts_read, COALESCE(sum(time_read),0) AS time_read, COUNT(uv.*) AS visits
FROM date_range dr
FULL OUTER JOIN users u ON (1=1)
LEFT JOIN user_visits uv ON (uv.user_id = u.id AND visited_at = dr.date)
GROUP BY u.id, dr.date, dr.week
ORDER BY u.id, dr.date
)

SELECT ps.id, ps.username, ps.created_at, CASE
WHEN :coverage::text = 'week' THEN ps.week::text
WHEN :coverage::text = 'all' THEN '-1'
ELSE ps.date::text
END  AS period, sum(ps.replies) AS replies, sum(ps.topics) AS topics, sum(ps.likes_received) AS likes_received, sum(lg.likes) AS likes_given, COALESCE(sum(posts_read),0) AS posts_read, COALESCE(sum(time_read),0) AS time_read, SUM(visits) AS visits
FROM posts_summary ps
LEFT JOIN likes_given lg ON (ps.date = lg.date AND ps.id = lg.id)
LEFT JOIN visits v ON (v.id = ps.id AND v.date = ps.date)
GROUP BY ps.id, ps.username, ps.created_at, CASE 
WHEN :coverage::text = 'week' THEN ps.week::text
WHEN :coverage::text = 'all' THEN '-1'
ELSE ps.date::text
END  
ORDER BY ps.id, CASE
WHEN :coverage::text = 'week' THEN ps.week::text
WHEN :coverage::text = 'all' THEN '-1'
ELSE ps.date::text
END
「いいね!」 1

ああ、わかりました。

冒頭の SELECT 1 はクエリの一部ではなく、Data Explorer で新しいクエリを作成する際のプレースホルダーです。これが問題の原因となっています。これを削除すれば正常に動作するはずです。

-- coverage: 'week', 'all', または 'date'
-- [params]
-- date :start_date = 2019-08-27
-- date :end_date = 2019-09-30
-- text :coverage = week

WITH date_range AS (
SELECT date_trunc('day', dd):: date AS "date", EXTRACT(week from date_trunc('day', dd):: date) AS "week"
FROM generate_series
        ( :start_date::timestamp 
        , :end_date::timestamp
        , '1 day'::interval) dd
), likes_given AS (
SELECT u.id, dr.date, dr.week, count(pa.*) AS "likes"
FROM date_range dr
FULL JOIN users u ON (1=1)
LEFT JOIN post_actions pa ON (pa.created_at:: date = dr.date and post_action_type_id=2 AND user_id = u.id)
GROUP BY dr.date, dr.week, u.id
ORDER BY u.id, dr.date
), posts_summary AS (SELECT u.id, u.username, u.created_at, dr.*, count(p.id) - count(t.id) AS replies, count(t.id) AS topics, COALESCE(sum(p.like_count),0) AS likes_received
from date_range dr
FULL OUTER JOIN users u ON (1=1)
LEFT JOIN posts p ON (p.user_id = u.id AND p.created_at::date=dr.date AND p.deleted_at IS NULL)
LEFT JOIN topics t ON (t.user_id = u.id AND t.created_at::date = dr.date AND p.topic_id = t.id AND t.deleted_at IS NULL)
GROUP BY u.id, dr.date, dr.week
ORDER BY u.id, dr.date), 
visits AS (SELECT u.id, dr.*, COALESCE(sum(posts_read),0) AS posts_read, COALESCE(sum(time_read),0) AS time_read, COUNT(uv.*) AS visits
FROM date_range dr
FULL OUTER JOIN users u ON (1=1)
LEFT JOIN user_visits uv ON (uv.user_id = u.id AND visited_at = dr.date)
GROUP BY u.id, dr.date, dr.week
ORDER BY u.id, dr.date
)

SELECT ps.id, ps.username, ps.created_at, CASE
WHEN :coverage::text = 'week' THEN ps.week::text
WHEN :coverage::text = 'all' THEN '-1'
ELSE ps.date::text
END  AS period, sum(ps.replies) AS replies, sum(ps.topics) AS topics, sum(ps.likes_received) AS likes_received, sum(lg.likes) AS likes_given, COALESCE(sum(posts_read),0) AS posts_read, COALESCE(sum(time_read),0) AS time_read, SUM(visits) AS visits
FROM posts_summary ps
LEFT JOIN likes_given lg ON (ps.date = lg.date AND ps.id = lg.id)
LEFT JOIN visits v ON (v.id = ps.id AND v.date = ps.date)
GROUP BY ps.id, ps.username, ps.created_at, CASE 
WHEN :coverage::text = 'week' THEN ps.week::text
WHEN :coverage::text = 'all' THEN '-1'
ELSE ps.date::text
END  
ORDER BY ps.id, CASE
WHEN :coverage::text = 'week' THEN ps.week::text
WHEN :coverage::text = 'all' THEN '-1'
ELSE ps.date::text
END

@Grayden_Shand さん、ありがとうございます。

エラーは解消されました。

もう少しお知恵を拝借したいのですが、このクエリで生成される集計値には、メールリストモードのユーザーによるメール投稿やメール返信も含まれていますか?含まれていない場合、それらを含めるにはどうすればよいでしょうか?

また、ユーザー名の横にカスタムユーザープロフィールフィールドの値を表示するにはどうすればよいでしょうか?

関連するフィールド名を特定し、これを実装するためのヒントがあれば、ぜひ教えていただけますか?

はい、含まれるはずです。David が述べた通り、posts テーブルには via_email というブーリアン型のカラムがあります。現在のクエリはこのカラムを無視しており、メール経由かどうかに関わらずすべての投稿をカウントしています。

user_custom_fields というテーブルがあります。特定のカスタムフィールドを含めるには、このテーブルと結合(JOIN)する必要があります。

おそらく post_summary サブクエリで行うのがよいでしょう。

例:

...
), posts_summary AS (
SELECT u.id, u.username, u.created_at, dr.*, count(p.id) - count(t.id) AS replies, count(t.id) AS topics, COALESCE(sum(p.like_count),0) AS likes_received, cf.value as LABEL_FOR_CUSTOM_FIELD
from date_range dr
FULL OUTER JOIN users u ON (1=1)
LEFT JOIN posts p ON (p.user_id = u.id AND p.created_at::date=dr.date AND p.deleted_at IS NULL)
LEFT JOIN topics t ON (t.user_id = u.id AND t.created_at::date = dr.date AND p.topic_id = t.id AND t.deleted_at IS NULL)
LEFT JOIN user_custom_fields cf ON (u.id == cf.user_id and cf.name == "YOUR CUSTOM FIELD NAME") 
GROUP BY u.id, dr.date, dr.week
ORDER BY u.id, dr.date
)
...

SELECT 句にカラムを追加し、user_custom_fields テーブル用の新しい JOIN 句を追加しました。

"YOUR CUSTOM FIELD NAME"LABEL_FOR_CUSTOM_FIELD を置き換える必要がある点にご注意ください。

その後、最終的なクエリで選択するカラムも更新する必要があります。

...
SELECT ps.id, ps.username, ps.created_at, ps.LABEL_FOR_CUSTOM_FIELD, CASE
WHEN :coverage::text = 'week' THEN ps.week::text
WHEN :coverage::text = 'all' THEN '-1'
ELSE ps.date::text
END  AS period, sum(ps.replies) AS replies, sum(ps.topics) AS topics, sum(ps.likes_received) AS likes_received, sum(lg.likes) AS likes_given, COALESCE(sum(posts_read),0) AS posts_read, COALESCE(sum(time_read),0) AS time_read, SUM(visits) AS visits
FROM posts_summary ps
..

私がこの問題に取り組むなら、おそらくこのようにするでしょう。

頑張ってください!