Estadísticas de temas resueltos y no resueltos con parámetros de fecha y etiqueta

Este informe del Explorador de Datos proporciona un análisis completo de los temas resueltos y no resueltos en un sitio, dentro de un rango de fechas especificado y, opcionalmente, filtrado por una etiqueta específica.

:discourse: Este informe requiere que el plugin Discourse Solved esté habilitado.

Este informe es particularmente útil para administradores y moderadores que buscan comprender la capacidad de respuesta de la comunidad e identificar áreas de mejora en el soporte y la participación de los usuarios.

Estadísticas de temas resueltos y no resueltos con parámetros de fecha y etiqueta

--[params]
-- date :start_date = 2022-01-01
-- date :end_date = 2024-01-01
-- text :tag_name = all

WITH valid_topics AS (
    SELECT 
        t.id,
        t.user_id,
        t.title,
        t.views,
        (SELECT COUNT(*) FROM posts WHERE topic_id = t.id AND deleted_at IS NULL AND post_type = 1) - 1 AS "posts_count", 
        t.created_at,
        (CURRENT_DATE::date - t.created_at::date) AS "total_days",
        STRING_AGG(tags.name, ', ') AS tag_names, -- Agrega etiquetas para cada tema
        c.name AS category_name
    FROM topics t
    LEFT JOIN topic_tags tt ON tt.topic_id = t.id
    LEFT JOIN tags ON tags.id = tt.tag_id
    LEFT JOIN categories c ON c.id = t.category_id
    WHERE t.deleted_at IS NULL
        AND t.created_at::date BETWEEN :start_date AND :end_date
        AND t.archetype = 'regular'
    GROUP BY t.id, c.name
),

solved_topics AS (
    SELECT 
        vt.id,
        dsst.created_at
    FROM discourse_solved_solved_topics dsst
    INNER JOIN valid_topics vt ON vt.id = dsst.topic_id
),

last_reply AS (
    SELECT p.topic_id, p.user_id FROM posts p
    INNER JOIN (SELECT topic_id, MAX(id) AS post FROM posts p
                WHERE deleted_at IS NULL
                AND post_type = 1
                GROUP BY topic_id) x ON x.post = p.id
),

first_reply AS (
    SELECT p.topic_id, p.user_id, p.created_at FROM posts p
    INNER JOIN (SELECT topic_id, MIN(id) AS post FROM posts p
                WHERE deleted_at IS NULL
                AND post_type = 1
                AND post_number > 1
                GROUP BY topic_id) x ON x.post = p.id
)

SELECT
    CASE 
        WHEN st.id IS NOT NULL THEN 'solved'
        ELSE 'unsolved'
    END AS status,
    vt.tag_names, 
    vt.category_name,
    vt.id AS topic_id,
    vt.user_id AS topic_user_id,
    ue.email,
    vt.title,
    vt.views,
    lr.user_id AS last_reply_user_id,
    ue2.email AS last_reply_user_email,
    vt.created_at::date AS topic_create,
    COALESCE(TO_CHAR(fr.created_at, 'YYYY-MM-DD'), '') AS first_reply_create,
    COALESCE(TO_CHAR(st.created_at, 'YYYY-MM-DD'), '') AS solution_create,
    COALESCE(fr.created_at::date - vt.created_at::date, 0) AS "time_first_reply(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (fr.created_at - vt.created_at)) / 3600.00), 0) AS "time_first_reply(hours)",
    COALESCE(st.created_at::date - vt.created_at::date, 0) AS "time_solution(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (st.created_at - vt.created_at)) / 3600.00), 0) AS "time_solution(hours)",
    vt.created_at::date,
    vt.posts_count AS number_of_replies,
    vt.total_days AS total_days_without_solution
FROM valid_topics vt
LEFT JOIN last_reply lr ON lr.topic_id = vt.id
LEFT JOIN first_reply fr ON fr.topic_id = vt.id
LEFT JOIN solved_topics st ON st.id = vt.id
INNER JOIN user_emails ue ON vt.user_id = ue.user_id AND ue."primary" = true
LEFT JOIN user_emails ue2 ON lr.user_id = ue2.user_id AND ue2."primary" = true
WHERE (:tag_name = 'all' OR vt.tag_names ILIKE '%' || :tag_name || '%')
GROUP BY st.id, vt.tag_names, vt.category_name, vt.id, vt.user_id, ue.email, vt.title, vt.views, lr.user_id, ue2.email, vt.created_at, fr.created_at, st.created_at, vt.posts_count, vt.total_days
ORDER BY topic_create, vt.total_days DESC

Explicación de la consulta SQL

La generación del informe se realiza a través de una compleja consulta SQL que utiliza Expresiones Comunes de Tabla (CTE) para organizar y procesar los datos de manera eficiente. La consulta está estructurada de la siguiente manera:

  • valid_topics: Esta CTE filtra los temas por el rango de fechas especificado y el arquetipo (‘regular’), excluyendo los temas eliminados. También agrega las etiquetas asociadas a cada tema para un filtrado posterior por nombre de etiqueta si se especifica.
  • solved_topics: Identifica los temas que han sido marcados como resueltos.
  • last_reply: Determina el usuario que realizó la última respuesta en cada tema encontrando el ID de publicación máximo (que indica la publicación más reciente) que no está eliminado y es de tipo de publicación 1 (indicando una publicación regular).
  • first_reply: Similar a last_reply, pero identifica al primer usuario que responde al tema después de la publicación original.

La consulta principal combina estas CTE para compilar un informe detallado de cada tema, incluyendo si está resuelto o no, nombres de etiquetas, nombre de categoría, IDs de tema y usuario, correos electrónicos, vistas, recuentos de respuestas y tiempos de la primera respuesta y solución.

Parámetros

  • start_date: El inicio del rango de fechas para el cual generar el informe.
  • end_date: El final del rango de fechas para el cual generar el informe.
  • tag_name: La etiqueta específica para filtrar los temas. Usa ‘all’ para incluir temas con cualquier etiqueta.

Resultados

El informe proporciona la siguiente información para cada tema dentro de los parámetros especificados:

  • status: Indica si el tema ha sido resuelto o permanece sin resolver.
  • tag_names: Muestra las etiquetas asociadas al tema.
  • category_name: Muestra la categoría asociada al tema.
  • topic_id: El identificador único del tema.
  • topic_user_id: El ID del usuario que creó el tema.
  • user_email: La dirección de correo electrónico del creador del tema.
  • title: El título del tema.
  • views: El número de vistas que ha recibido el tema.
  • last_reply_user_id: El ID del usuario que realizó la última respuesta en el tema.
  • last_reply_user_email: La dirección de correo electrónico del usuario que realizó la última respuesta.
  • topic_create: La fecha en que se creó el tema.
  • first_reply_create: La fecha de la primera respuesta al tema.
  • solution_create: La fecha en que se marcó una solución para el tema (si corresponde).
  • time_first_reply(days/hours): El tiempo transcurrido para recibir la primera respuesta, en días y horas.
  • time_solution(days/hours): El tiempo transcurrido para resolver el tema, en días y horas.
  • created_at: La fecha de creación del tema.
  • number_of_replies: El número total de respuestas al tema.
  • total_days_without_solution: El número total de días que el tema ha estado activo sin una solución.

Resultados de ejemplo

status tag_names category_name topic_id topic_user_id user_email title views last_reply_user_id last_reply_user_email topic_create first_reply_create solution_create time_first_reply(days) time_first_reply(hours) time_solution(days) time_solution(hours) created_at number_of_replies total_days_without_solution
solved support, password category1 101 1 user1@example.com How to reset my password? 150 3 user3@example.com 2022-01-05 2022-01-06 2022-01-07 1 24 2 48 2022-01-05 5 2
unsolved support, account category2 102 2 user2@example.com Issue with account activation 75 4 user4@example.com 2022-02-10 2022-02-12 2 48 0 0 2022-02-10 3 412
solved support category3 103 5 user5@example.com Can’t upload profile picture 200 6 user6@example.com 2022-03-15 2022-03-16 2022-03-18 1 24 3 72 2022-03-15 8 3
unsolved NULL category4 104 7 user7@example.com Error when posting 50 8 user8@example.com 2022-04-20 0 0 0 0 2022-04-20 0 373
3 Me gusta

Otra consulta increíble y otra solicitud de mi parte. :slight_smile:

¿Puedes crear un campo de selección para acotar la categoría/subcategoría?
Me encantaría poder ejecutar este informe solo en la categoría de mis tickets.

Además, encontré un caso extremo peculiar. Quizás puedas o no tenerlo en cuenta, pero no pierdo nada con preguntar.

Tengo un tema al que respondí y marqué como solución al día siguiente de su publicación. Luego, otro técnico dio una respuesta diferente y marcó esa como la solución unos 10 días después.

El informe muestra el tiempo hasta la solución como 1 día, pero el tiempo total sin solución como 10 días.

PNG image

Hola @tknospdr,

Para responder a ambas tus preguntas aquí:

Puedes usar la siguiente consulta para abordar esto:

--[params]
-- date :start_date = 2022-01-01
-- date :end_date = 2024-01-01
-- text :tag_name = all
-- null category_id :category_id

WITH valid_topics AS (
    SELECT
        t.id,
        t.user_id,
        t.title,
        t.views,
        (SELECT COUNT(*) FROM posts WHERE topic_id = t.id AND deleted_at IS NULL AND post_type = 1) - 1 AS "posts_count",
        t.created_at,
        (CURRENT_DATE::date - t.created_at::date) AS "total_days",
        STRING_AGG(tags.name, ', ') AS tag_names,
        c.name AS category_name,
        t.category_id
    FROM topics t
    LEFT JOIN topic_tags tt ON tt.topic_id = t.id
    LEFT JOIN tags ON tags.id = tt.tag_id
    LEFT JOIN categories c ON c.id = t.category_id
    WHERE t.deleted_at IS NULL
        AND t.created_at::date BETWEEN :start_date AND :end_date
        AND t.archetype = 'regular'
    GROUP BY t.id, c.name, t.category_id
),

solved_topics AS (
    SELECT
        dsst.topic_id,
        MIN(dsst.created_at) AS first_solution_at, -- Get earliest solution
        MAX(dsst.created_at) AS latest_solution_at -- Get latest solution
    FROM discourse_solved_solved_topics dsst
    GROUP BY dsst.topic_id
),

last_reply AS (
    SELECT p.topic_id, p.user_id FROM posts p
    INNER JOIN (SELECT topic_id, MAX(id) AS post FROM posts p
                WHERE deleted_at IS NULL
                AND post_type = 1
                GROUP BY topic_id) x ON x.post = p.id
),

first_reply AS (
    SELECT p.topic_id, p.user_id, p.created_at FROM posts p
    INNER JOIN (SELECT topic_id, MIN(id) AS post FROM posts p
                WHERE deleted_at IS NULL
                AND post_type = 1
                AND post_number > 1
                GROUP BY topic_id) x ON x.post = p.id
)

SELECT
    CASE
        WHEN st.topic_id IS NOT NULL THEN 'solved'
        ELSE 'unsolved'
    END AS status,
    vt.tag_names,
    vt.category_name,
    vt.id AS topic_id,
    vt.user_id AS topic_user_id,
    ue.email,
    vt.title,
    vt.views,
    lr.user_id AS last_reply_user_id,
    ue2.email AS last_reply_user_email,
    vt.created_at::date AS topic_create,
    COALESCE(TO_CHAR(fr.created_at, 'YYYY-MM-DD'), '') AS first_reply_create,
    COALESCE(TO_CHAR(st.first_solution_at, 'YYYY-MM-DD'), '') AS first_solution_create,
    COALESCE(TO_CHAR(st.latest_solution_at, 'YYYY-MM-DD'), '') AS latest_solution_create,
    COALESCE(fr.created_at::date - vt.created_at::date, 0) AS "time_first_reply(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (fr.created_at - vt.created_at)) / 3600.00), 0) AS "time_first_reply(hours)",
    COALESCE(st.first_solution_at::date - vt.created_at::date, 0) AS "time_to_first_solution(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (st.first_solution_at - vt.created_at)) / 3600.00), 0) AS "time_to_first_solution(hours)",
    COALESCE(st.latest_solution_at::date - vt.created_at::date, 0) AS "time_to_latest_solution(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (st.latest_solution_at - vt.created_at)) / 3600.00), 0) AS "time_to_latest_solution(hours)",
    vt.created_at::date,
    vt.posts_count AS number_of_replies,
    CASE
        WHEN st.topic_id IS NULL THEN vt.total_days
        ELSE COALESCE(st.latest_solution_at::date - vt.created_at::date, 0)
    END AS total_days_without_solution
FROM valid_topics vt
LEFT JOIN last_reply lr ON lr.topic_id = vt.id
LEFT JOIN first_reply fr ON fr.topic_id = vt.id
LEFT JOIN solved_topics st ON st.topic_id = vt.id
INNER JOIN user_emails ue ON vt.user_id = ue.user_id AND ue."primary" = true
LEFT JOIN user_emails ue2 ON lr.user_id = ue2.user_id AND ue2."primary" = true
WHERE (:tag_name = 'all' OR vt.tag_names ILIKE '%' || :tag_name || '%')
  AND (:category_id ISNULL OR vt.category_id = :category_id)
GROUP BY st.topic_id, st.first_solution_at, st.latest_solution_at, vt.tag_names, vt.category_name, vt.id, vt.user_id, ue.email, vt.title, vt.views, lr.user_id, ue2.email, vt.created_at, fr.created_at, vt.posts_count, vt.total_days
ORDER BY topic_create, vt.total_days DESC

Donde el parámetro -- null category_id :category_id se puede usar para (opcionalmente) seleccionar una categoría para ejecutar el informe, y los resultados rastrean tanto la primera como la última solución.

Además, el resultado total_days_without_solution ahora usará la fecha de la última solución en lugar de la primera.

1 me gusta

¡Genial, gracias! Se ve muy bien.

1 me gusta