diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index 5b8b886f21..344deb5624 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -1665,22 +1665,37 @@ checksums: workspace/analysis/charts/failed_to_load_dashboards: 876c54d9cc69ceda6f808231e2557eb2 workspace/analysis/charts/failed_to_save_chart: e237cf1a56a8f9ee30067fdb0757f7c5 workspace/analysis/charts/field: cfd632297d7809a3539e90c9cd4728d9 - workspace/analysis/charts/field_label_average_score: 5b5aa7322549521d1e813b1c8312d443 + workspace/analysis/charts/field_label_ces_average: 3bf598396ea490f3a2bdccf0c94b6aa0 + workspace/analysis/charts/field_label_ces_count: 4dc90d50a8e05dd9ba4a9e356926e0cb workspace/analysis/charts/field_label_collected_at: b41902ddb4586ba4a4611d726b5014aa workspace/analysis/charts/field_label_count: 9c5848662eb8024ddf360f7e4001a968 + workspace/analysis/charts/field_label_created_at: 9ce495d7fc74e1a2ae86c07206a3e531 + workspace/analysis/charts/field_label_csat_average: f7c43dac56267f832fbebd6d18efdef1 + workspace/analysis/charts/field_label_csat_count: 30c1ab12748b503ffee399ed326e0562 + workspace/analysis/charts/field_label_csat_dissatisfied_count: 7f68b2c8302bde5cd93ba86d2163f86d + workspace/analysis/charts/field_label_csat_neutral_count: 19edae275784e8d53dd45003a2e8971a + workspace/analysis/charts/field_label_csat_satisfied_count: 78fcabc88da4b22171be149b1be509bd + workspace/analysis/charts/field_label_csat_score: 89a87f1069641bba10607d5b407cb0aa workspace/analysis/charts/field_label_detractor_count: eedb15bc383eb0f14d43043e6666c62a - workspace/analysis/charts/field_label_emotion: eb3a31ead51b5c8a8d365d5f904e9206 workspace/analysis/charts/field_label_field_type: 2581066dc304c853a4a817c20996fa08 + workspace/analysis/charts/field_label_language: 277fd1a41cc237a437cd1d5e4a80463b + workspace/analysis/charts/field_label_nps_average: 4a61877a06cb8e64a0a8375dd058a548 workspace/analysis/charts/field_label_nps_score: 9c8d0b0b460f9689bd66e81d45e0a2df - workspace/analysis/charts/field_label_nps_value: cb7404025044400e3d7d5600f3133e4f workspace/analysis/charts/field_label_passive_count: ceb71da8d1382eb2097089dc3ecf76da workspace/analysis/charts/field_label_promoter_count: c393131a4bd3a25bf6b297beed20e34f + workspace/analysis/charts/field_label_question: 0576462ce60d4263d7c482463fcc9547 + workspace/analysis/charts/field_label_question_group: b007e2cfd1262272de3260f8d14d5833 workspace/analysis/charts/field_label_response_id: 73375099cc976dc7203b8e27f5f709e0 - workspace/analysis/charts/field_label_sentiment: 9ba5719c80c0136c2d0644217619aff6 workspace/analysis/charts/field_label_source_name: 157675beca12efcd8ec512c5256b1a61 workspace/analysis/charts/field_label_source_type: d1ff69af76c687eb189db72030717570 - workspace/analysis/charts/field_label_topic: 7f542b783cd528f00f4f485e35b48dc1 + workspace/analysis/charts/field_label_unique_respondents: e340f09af176927f1ed16719ee304274 + workspace/analysis/charts/field_label_unique_responses: d9ffcc58f72b9fdb143027703371f22b + workspace/analysis/charts/field_label_updated_at: a3730393cce5adfd9e50123d96640fd6 workspace/analysis/charts/field_label_user_identifier: b0174469c95038766744fb7e64005aec + workspace/analysis/charts/field_label_value_boolean: bbdcd3f46954b6304b9069e94e1371ab + workspace/analysis/charts/field_label_value_date: c8d705d1975affc01c002324725fec3f + workspace/analysis/charts/field_label_value_number: 1f14da79d14bd7b1c2324141f4470675 + workspace/analysis/charts/field_label_value_text: e097a597cc507c716401ad18255de578 workspace/analysis/charts/filter_data: 05cc68ed2896feef60bbe3829cd9063d workspace/analysis/charts/filters: acf5accc113ff3c1992688058576732c workspace/analysis/charts/filters_toggle_description: ea18bdb212a6a85620125cab89a4b1c1 diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index 28fb525f34..1c45bd4ce9 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Diagramm konnte nicht gespeichert werden", "field": "Feld", - "field_label_average_score": "Durchschnittliche Bewertung", + "field_label_ces_average": "CES-Durchschnitt", + "field_label_ces_count": "CES-Anzahl", "field_label_collected_at": "Erfasst am", "field_label_count": "Anzahl", + "field_label_created_at": "Erstellt am", + "field_label_csat_average": "CSAT-Durchschnitt", + "field_label_csat_count": "CSAT-Anzahl", + "field_label_csat_dissatisfied_count": "CSAT Unzufriedene Anzahl", + "field_label_csat_neutral_count": "CSAT Neutrale Anzahl", + "field_label_csat_satisfied_count": "CSAT-Zufriedene Anzahl", + "field_label_csat_score": "CSAT-Score", "field_label_detractor_count": "Anzahl Kritiker", - "field_label_emotion": "Emotion", "field_label_field_type": "Feldtyp", + "field_label_language": "Sprache", + "field_label_nps_average": "NPS-Durchschnitt", "field_label_nps_score": "NPS-Score", - "field_label_nps_value": "NPS-Wert", "field_label_passive_count": "Anzahl Passive", "field_label_promoter_count": "Anzahl Promoter", + "field_label_question": "Frage", + "field_label_question_group": "Fragengruppe", "field_label_response_id": "Antwort-ID", - "field_label_sentiment": "Stimmung", "field_label_source_name": "Quellenname", "field_label_source_type": "Quellentyp", - "field_label_topic": "Thema", + "field_label_unique_respondents": "Eindeutige Teilnehmer", + "field_label_unique_responses": "Eindeutige Antworten", + "field_label_updated_at": "Aktualisiert am", "field_label_user_identifier": "Benutzerkennung", + "field_label_value_boolean": "Wert (Boolean)", + "field_label_value_date": "Wert (Datum)", + "field_label_value_number": "Wert (Zahl)", + "field_label_value_text": "Wert (Text)", "filter_data": "Daten filtern", "filters": "Filter", "filters_toggle_description": "Nur Daten einbeziehen, die die folgenden Bedingungen erfüllen.", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 97db2e38bc..6233d1ab59 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Failed to save chart", "field": "Field", - "field_label_average_score": "Average Score", + "field_label_ces_average": "CES Average", + "field_label_ces_count": "CES Count", "field_label_collected_at": "Collected At", "field_label_count": "Count", + "field_label_created_at": "Created At", + "field_label_csat_average": "CSAT Average", + "field_label_csat_count": "CSAT Count", + "field_label_csat_dissatisfied_count": "CSAT Dissatisfied Count", + "field_label_csat_neutral_count": "CSAT Neutral Count", + "field_label_csat_satisfied_count": "CSAT Satisfied Count", + "field_label_csat_score": "CSAT Score", "field_label_detractor_count": "Detractor Count", - "field_label_emotion": "Emotion", "field_label_field_type": "Field Type", + "field_label_language": "Language", + "field_label_nps_average": "NPS Average", "field_label_nps_score": "NPS Score", - "field_label_nps_value": "NPS Value", "field_label_passive_count": "Passive Count", "field_label_promoter_count": "Promoter Count", + "field_label_question": "Question", + "field_label_question_group": "Question Group", "field_label_response_id": "Response ID", - "field_label_sentiment": "Sentiment", "field_label_source_name": "Source Name", "field_label_source_type": "Source Type", - "field_label_topic": "Topic", + "field_label_unique_respondents": "Unique Respondents", + "field_label_unique_responses": "Unique Responses", + "field_label_updated_at": "Updated At", "field_label_user_identifier": "User Identifier", + "field_label_value_boolean": "Value (Boolean)", + "field_label_value_date": "Value (Date)", + "field_label_value_number": "Value (Number)", + "field_label_value_text": "Value (Text)", "filter_data": "Filter data", "filters": "Filters", "filters_toggle_description": "Only include data that meets the following conditions.", diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index e90e68cfd8..36f39bb491 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Error al guardar el gráfico", "field": "Campo", - "field_label_average_score": "Puntuación media", + "field_label_ces_average": "Promedio CES", + "field_label_ces_count": "Recuento CES", "field_label_collected_at": "Recopilado el", "field_label_count": "Recuento", + "field_label_created_at": "Fecha de creación", + "field_label_csat_average": "Promedio CSAT", + "field_label_csat_count": "Recuento CSAT", + "field_label_csat_dissatisfied_count": "Recuento de insatisfechos CSAT", + "field_label_csat_neutral_count": "Recuento de neutros CSAT", + "field_label_csat_satisfied_count": "Recuento de Satisfechos CSAT", + "field_label_csat_score": "Puntuación CSAT", "field_label_detractor_count": "Recuento de detractores", - "field_label_emotion": "Emoción", "field_label_field_type": "Tipo de campo", + "field_label_language": "Idioma", + "field_label_nps_average": "Promedio NPS", "field_label_nps_score": "Puntuación NPS", - "field_label_nps_value": "Valor NPS", "field_label_passive_count": "Recuento de pasivos", "field_label_promoter_count": "Recuento de promotores", + "field_label_question": "Pregunta", + "field_label_question_group": "Grupo de preguntas", "field_label_response_id": "ID de respuesta", - "field_label_sentiment": "Sentimiento", "field_label_source_name": "Nombre de origen", "field_label_source_type": "Tipo de origen", - "field_label_topic": "Tema", + "field_label_unique_respondents": "Encuestados Únicos", + "field_label_unique_responses": "Respuestas Únicas", + "field_label_updated_at": "Fecha de actualización", "field_label_user_identifier": "Identificador de usuario", + "field_label_value_boolean": "Valor (Booleano)", + "field_label_value_date": "Valor (Fecha)", + "field_label_value_number": "Valor (Número)", + "field_label_value_text": "Valor (Texto)", "filter_data": "Filtrar datos", "filters": "Filtros", "filters_toggle_description": "Incluye solo los datos que cumplan las siguientes condiciones.", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index acc1d7b3de..bb8840ffe9 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Échec de l'enregistrement du graphique", "field": "Champ", - "field_label_average_score": "Score moyen", + "field_label_ces_average": "Moyenne CES", + "field_label_ces_count": "Nombre CES", "field_label_collected_at": "Collecté le", "field_label_count": "Nombre", + "field_label_created_at": "Créé le", + "field_label_csat_average": "Moyenne CSAT", + "field_label_csat_count": "Nombre CSAT", + "field_label_csat_dissatisfied_count": "Nombre d'insatisfaits CSAT", + "field_label_csat_neutral_count": "Nombre de neutres CSAT", + "field_label_csat_satisfied_count": "Nombre de clients satisfaits CSAT", + "field_label_csat_score": "Score CSAT", "field_label_detractor_count": "Nombre de détracteurs", - "field_label_emotion": "Émotion", "field_label_field_type": "Type de champ", + "field_label_language": "Langue", + "field_label_nps_average": "Moyenne NPS", "field_label_nps_score": "Score NPS", - "field_label_nps_value": "Valeur NPS", "field_label_passive_count": "Nombre de passifs", "field_label_promoter_count": "Nombre de promoteurs", + "field_label_question": "Question", + "field_label_question_group": "Groupe de questions", "field_label_response_id": "ID de réponse", - "field_label_sentiment": "Sentiment", "field_label_source_name": "Nom de la source", "field_label_source_type": "Type de source", - "field_label_topic": "Sujet", + "field_label_unique_respondents": "Répondants uniques", + "field_label_unique_responses": "Réponses uniques", + "field_label_updated_at": "Mis à jour le", "field_label_user_identifier": "Identifiant utilisateur", + "field_label_value_boolean": "Valeur (booléenne)", + "field_label_value_date": "Valeur (date)", + "field_label_value_number": "Valeur (Nombre)", + "field_label_value_text": "Valeur (Texte)", "filter_data": "Filtrer les données", "filters": "Filtres", "filters_toggle_description": "Inclure uniquement les données qui répondent aux conditions suivantes.", diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json index 7f37dc3138..605a1a914e 100644 --- a/apps/web/locales/hu-HU.json +++ b/apps/web/locales/hu-HU.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "A diagram mentése sikertelen", "field": "Mező", - "field_label_average_score": "Átlagos pontszám", + "field_label_ces_average": "CES átlag", + "field_label_ces_count": "CES darabszám", "field_label_collected_at": "Gyűjtve", "field_label_count": "Darabszám", + "field_label_created_at": "Létrehozás dátuma", + "field_label_csat_average": "CSAT átlag", + "field_label_csat_count": "CSAT darabszám", + "field_label_csat_dissatisfied_count": "CSAT elégedetlen válaszok száma", + "field_label_csat_neutral_count": "CSAT semleges válaszok száma", + "field_label_csat_satisfied_count": "CSAT elégedett válaszadók száma", + "field_label_csat_score": "CSAT pontszám", "field_label_detractor_count": "Kritikusok száma", - "field_label_emotion": "Érzelem", "field_label_field_type": "Mező típusa", + "field_label_language": "Nyelv", + "field_label_nps_average": "NPS átlag", "field_label_nps_score": "NPS pontszám", - "field_label_nps_value": "NPS érték", "field_label_passive_count": "Passzívak száma", "field_label_promoter_count": "Támogatók száma", + "field_label_question": "Kérdés", + "field_label_question_group": "Kérdéscsoport", "field_label_response_id": "Válaszazonosító", - "field_label_sentiment": "Hangulat", "field_label_source_name": "Forrás neve", "field_label_source_type": "Forrás típusa", - "field_label_topic": "Téma", + "field_label_unique_respondents": "Egyedi válaszadók", + "field_label_unique_responses": "Egyedi válaszok", + "field_label_updated_at": "Frissítés dátuma", "field_label_user_identifier": "Felhasználóazonosító", + "field_label_value_boolean": "Érték (logikai)", + "field_label_value_date": "Érték (dátum)", + "field_label_value_number": "Érték (szám)", + "field_label_value_text": "Érték (szöveg)", "filter_data": "Adatok szűrése", "filters": "Szűrők", "filters_toggle_description": "Csak azokat az adatokat tartalmazza, amelyek megfelelnek a következő feltételeknek.", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index fda40124e4..d767b2ca78 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "チャートの保存に失敗しました", "field": "フィールド", - "field_label_average_score": "平均スコア", + "field_label_ces_average": "CES平均", + "field_label_ces_count": "CES件数", "field_label_collected_at": "収集日時", "field_label_count": "カウント", + "field_label_created_at": "作成日時", + "field_label_csat_average": "CSAT平均", + "field_label_csat_count": "CSAT件数", + "field_label_csat_dissatisfied_count": "CSAT 不満足数", + "field_label_csat_neutral_count": "CSAT 中立数", + "field_label_csat_satisfied_count": "CSAT満足件数", + "field_label_csat_score": "CSATスコア", "field_label_detractor_count": "批判者数", - "field_label_emotion": "感情", "field_label_field_type": "フィールドタイプ", + "field_label_language": "言語", + "field_label_nps_average": "NPS平均", "field_label_nps_score": "NPSスコア", - "field_label_nps_value": "NPS値", "field_label_passive_count": "中立者数", "field_label_promoter_count": "推奨者数", + "field_label_question": "質問", + "field_label_question_group": "質問グループ", "field_label_response_id": "回答ID", - "field_label_sentiment": "感情分析", "field_label_source_name": "ソース名", "field_label_source_type": "ソースタイプ", - "field_label_topic": "トピック", + "field_label_unique_respondents": "ユニーク回答者数", + "field_label_unique_responses": "ユニーク回答数", + "field_label_updated_at": "更新日時", "field_label_user_identifier": "ユーザー識別子", + "field_label_value_boolean": "値(ブール値)", + "field_label_value_date": "値(日付)", + "field_label_value_number": "値(数値)", + "field_label_value_text": "値(テキスト)", "filter_data": "データをフィルター", "filters": "フィルター", "filters_toggle_description": "以下の条件を満たすデータのみを含めます。", diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index d66d5892a9..ec0a5d8b0c 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Opslaan van diagram mislukt", "field": "Veld", - "field_label_average_score": "Gemiddelde score", + "field_label_ces_average": "CES Gemiddelde", + "field_label_ces_count": "CES Aantal", "field_label_collected_at": "Verzameld op", "field_label_count": "Aantal", + "field_label_created_at": "Aangemaakt op", + "field_label_csat_average": "CSAT Gemiddelde", + "field_label_csat_count": "CSAT Aantal", + "field_label_csat_dissatisfied_count": "CSAT Aantal ontevreden", + "field_label_csat_neutral_count": "CSAT Aantal neutraal", + "field_label_csat_satisfied_count": "CSAT Tevreden Aantal", + "field_label_csat_score": "CSAT Score", "field_label_detractor_count": "Aantal detractors", - "field_label_emotion": "Emotie", "field_label_field_type": "Veldtype", + "field_label_language": "Taal", + "field_label_nps_average": "NPS Gemiddelde", "field_label_nps_score": "NPS-score", - "field_label_nps_value": "NPS-waarde", "field_label_passive_count": "Aantal passieven", "field_label_promoter_count": "Aantal promoters", + "field_label_question": "Vraag", + "field_label_question_group": "Vraaggroep", "field_label_response_id": "Antwoord-ID", - "field_label_sentiment": "Sentiment", "field_label_source_name": "Bronnaam", "field_label_source_type": "Brontype", - "field_label_topic": "Onderwerp", + "field_label_unique_respondents": "Unieke Respondenten", + "field_label_unique_responses": "Unieke Antwoorden", + "field_label_updated_at": "Bijgewerkt op", "field_label_user_identifier": "Gebruikersidentificatie", + "field_label_value_boolean": "Waarde (Boolean)", + "field_label_value_date": "Waarde (Datum)", + "field_label_value_number": "Waarde (Getal)", + "field_label_value_text": "Waarde (Tekst)", "filter_data": "Data filteren", "filters": "Filters", "filters_toggle_description": "Neem alleen gegevens op die aan de volgende voorwaarden voldoen.", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index d2d5ae659a..9e08bda027 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Falha ao salvar gráfico", "field": "Campo", - "field_label_average_score": "Pontuação média", + "field_label_ces_average": "Média CES", + "field_label_ces_count": "Contagem CES", "field_label_collected_at": "Coletado em", "field_label_count": "Contagem", + "field_label_created_at": "Criado em", + "field_label_csat_average": "Média CSAT", + "field_label_csat_count": "Contagem CSAT", + "field_label_csat_dissatisfied_count": "Contagem de Insatisfeitos CSAT", + "field_label_csat_neutral_count": "Contagem de Neutros CSAT", + "field_label_csat_satisfied_count": "Contagem de Satisfeitos CSAT", + "field_label_csat_score": "Pontuação CSAT", "field_label_detractor_count": "Contagem de detratores", - "field_label_emotion": "Emoção", "field_label_field_type": "Tipo de campo", + "field_label_language": "Idioma", + "field_label_nps_average": "Média NPS", "field_label_nps_score": "Pontuação de NPS", - "field_label_nps_value": "Valor de NPS", "field_label_passive_count": "Contagem de passivos", "field_label_promoter_count": "Contagem de promotores", + "field_label_question": "Pergunta", + "field_label_question_group": "Grupo de Perguntas", "field_label_response_id": "ID da resposta", - "field_label_sentiment": "Sentimento", "field_label_source_name": "Nome da fonte", "field_label_source_type": "Tipo de fonte", - "field_label_topic": "Tópico", + "field_label_unique_respondents": "Respondentes Únicos", + "field_label_unique_responses": "Respostas Únicas", + "field_label_updated_at": "Atualizado em", "field_label_user_identifier": "Identificador do usuário", + "field_label_value_boolean": "Valor (Booleano)", + "field_label_value_date": "Valor (Data)", + "field_label_value_number": "Valor (Número)", + "field_label_value_text": "Valor (Texto)", "filter_data": "Filtrar dados", "filters": "Filtros", "filters_toggle_description": "Incluir apenas dados que atendam às seguintes condições.", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 7d60552a48..f46491882c 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Falha ao guardar gráfico", "field": "Campo", - "field_label_average_score": "Pontuação média", + "field_label_ces_average": "Média CES", + "field_label_ces_count": "Contagem CES", "field_label_collected_at": "Recolhido em", "field_label_count": "Contagem", + "field_label_created_at": "Criado em", + "field_label_csat_average": "Média CSAT", + "field_label_csat_count": "Contagem CSAT", + "field_label_csat_dissatisfied_count": "Contagem de CSAT Insatisfeito", + "field_label_csat_neutral_count": "Contagem de CSAT Neutro", + "field_label_csat_satisfied_count": "Contagem de Satisfeitos CSAT", + "field_label_csat_score": "Pontuação CSAT", "field_label_detractor_count": "Contagem de detratores", - "field_label_emotion": "Emoção", "field_label_field_type": "Tipo de campo", + "field_label_language": "Idioma", + "field_label_nps_average": "Média NPS", "field_label_nps_score": "Pontuação NPS", - "field_label_nps_value": "Valor NPS", "field_label_passive_count": "Contagem de passivos", "field_label_promoter_count": "Contagem de promotores", + "field_label_question": "Pergunta", + "field_label_question_group": "Grupo de Perguntas", "field_label_response_id": "ID de resposta", - "field_label_sentiment": "Sentimento", "field_label_source_name": "Nome da origem", "field_label_source_type": "Tipo de origem", - "field_label_topic": "Tópico", + "field_label_unique_respondents": "Inquiridos Únicos", + "field_label_unique_responses": "Respostas Únicas", + "field_label_updated_at": "Atualizado em", "field_label_user_identifier": "Identificador de utilizador", + "field_label_value_boolean": "Valor (Booleano)", + "field_label_value_date": "Valor (Data)", + "field_label_value_number": "Valor (Número)", + "field_label_value_text": "Valor (Texto)", "filter_data": "Filtrar dados", "filters": "Filtros", "filters_toggle_description": "Incluir apenas dados que cumpram as seguintes condições.", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 200d31d788..1fe2f1537e 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Nu s-a putut salva graficul", "field": "Câmp", - "field_label_average_score": "Scor mediu", + "field_label_ces_average": "Media CES", + "field_label_ces_count": "Număr CES", "field_label_collected_at": "Colectat la", "field_label_count": "Număr", + "field_label_created_at": "Creat la", + "field_label_csat_average": "Media CSAT", + "field_label_csat_count": "Număr CSAT", + "field_label_csat_dissatisfied_count": "Număr CSAT Nemulțumiți", + "field_label_csat_neutral_count": "Număr CSAT Neutri", + "field_label_csat_satisfied_count": "Număr clienți mulțumiți CSAT", + "field_label_csat_score": "Scor CSAT", "field_label_detractor_count": "Număr de detractori", - "field_label_emotion": "Emoție", "field_label_field_type": "Tip câmp", + "field_label_language": "Limbă", + "field_label_nps_average": "Media NPS", "field_label_nps_score": "Scor NPS", - "field_label_nps_value": "Valoare NPS", "field_label_passive_count": "Număr de pasivi", "field_label_promoter_count": "Număr de promotori", + "field_label_question": "Întrebare", + "field_label_question_group": "Grup de întrebări", "field_label_response_id": "ID răspuns", - "field_label_sentiment": "Sentiment", "field_label_source_name": "Nume sursă", "field_label_source_type": "Tip sursă", - "field_label_topic": "Subiect", + "field_label_unique_respondents": "Respondenți unici", + "field_label_unique_responses": "Răspunsuri unice", + "field_label_updated_at": "Actualizat la", "field_label_user_identifier": "Identificator utilizator", + "field_label_value_boolean": "Valoare (Boolean)", + "field_label_value_date": "Valoare (Dată)", + "field_label_value_number": "Valoare (Număr)", + "field_label_value_text": "Valoare (Text)", "filter_data": "Filtrează datele", "filters": "Filtre", "filters_toggle_description": "Include doar datele care îndeplinesc următoarele condiții.", diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index c82d9d276f..c37e9a9f5e 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Не удалось сохранить график", "field": "Поле", - "field_label_average_score": "Средний балл", + "field_label_ces_average": "Средний CES", + "field_label_ces_count": "Количество CES", "field_label_collected_at": "Дата сбора", "field_label_count": "Количество", + "field_label_created_at": "Дата создания", + "field_label_csat_average": "Средний CSAT", + "field_label_csat_count": "Количество CSAT", + "field_label_csat_dissatisfied_count": "Количество недовольных (CSAT)", + "field_label_csat_neutral_count": "Количество нейтральных (CSAT)", + "field_label_csat_satisfied_count": "Количество удовлетворённых (CSAT)", + "field_label_csat_score": "Оценка CSAT", "field_label_detractor_count": "Количество критиков", - "field_label_emotion": "Эмоция", "field_label_field_type": "Тип поля", + "field_label_language": "Язык", + "field_label_nps_average": "Средний NPS", "field_label_nps_score": "Оценка NPS", - "field_label_nps_value": "Значение NPS", "field_label_passive_count": "Количество пассивных", "field_label_promoter_count": "Количество промоутеров", + "field_label_question": "Вопрос", + "field_label_question_group": "Группа вопросов", "field_label_response_id": "ID ответа", - "field_label_sentiment": "Тональность", "field_label_source_name": "Название источника", "field_label_source_type": "Тип источника", - "field_label_topic": "Тема", + "field_label_unique_respondents": "Уникальные респонденты", + "field_label_unique_responses": "Уникальные ответы", + "field_label_updated_at": "Дата обновления", "field_label_user_identifier": "Идентификатор пользователя", + "field_label_value_boolean": "Значение (логическое)", + "field_label_value_date": "Значение (дата)", + "field_label_value_number": "Значение (число)", + "field_label_value_text": "Значение (текст)", "filter_data": "Фильтровать данные", "filters": "Фильтры", "filters_toggle_description": "Включай только те данные, которые соответствуют следующим условиям.", diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index d80b6fe19a..e42692c955 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Det gick inte att spara diagrammet", "field": "Fält", - "field_label_average_score": "Genomsnittligt betyg", + "field_label_ces_average": "CES-medelvärde", + "field_label_ces_count": "CES-antal", "field_label_collected_at": "Insamlad", "field_label_count": "Antal", + "field_label_created_at": "Skapad", + "field_label_csat_average": "CSAT-medelvärde", + "field_label_csat_count": "CSAT-antal", + "field_label_csat_dissatisfied_count": "CSAT-antal missnöjda", + "field_label_csat_neutral_count": "CSAT-antal neutrala", + "field_label_csat_satisfied_count": "CSAT antal nöjda", + "field_label_csat_score": "CSAT-poäng", "field_label_detractor_count": "Antal kritiker", - "field_label_emotion": "Känsla", "field_label_field_type": "Fälttyp", + "field_label_language": "Språk", + "field_label_nps_average": "NPS-medelvärde", "field_label_nps_score": "NPS-poäng", - "field_label_nps_value": "NPS-värde", "field_label_passive_count": "Antal passiva", "field_label_promoter_count": "Antal förespråkare", + "field_label_question": "Fråga", + "field_label_question_group": "Frågegrupp", "field_label_response_id": "Svar-ID", - "field_label_sentiment": "Sentiment", "field_label_source_name": "Källnamn", "field_label_source_type": "Källtyp", - "field_label_topic": "Ämne", + "field_label_unique_respondents": "Unika respondenter", + "field_label_unique_responses": "Unika svar", + "field_label_updated_at": "Uppdaterad", "field_label_user_identifier": "Användar-ID", + "field_label_value_boolean": "Värde (Boolean)", + "field_label_value_date": "Värde (Datum)", + "field_label_value_number": "Värde (Nummer)", + "field_label_value_text": "Värde (Text)", "filter_data": "Filtrera data", "filters": "Filter", "filters_toggle_description": "Inkludera bara data som uppfyller följande villkor.", diff --git a/apps/web/locales/tr-TR.json b/apps/web/locales/tr-TR.json index e5fcd31e08..c07b8f695b 100644 --- a/apps/web/locales/tr-TR.json +++ b/apps/web/locales/tr-TR.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "Grafik kaydedilemedi", "field": "Alan", - "field_label_average_score": "Ortalama Puan", + "field_label_ces_average": "CES Ortalaması", + "field_label_ces_count": "CES Sayısı", "field_label_collected_at": "Toplandığı Tarih", "field_label_count": "Sayı", + "field_label_created_at": "Oluşturulma Tarihi", + "field_label_csat_average": "CSAT Ortalaması", + "field_label_csat_count": "CSAT Sayısı", + "field_label_csat_dissatisfied_count": "CSAT Memnuniyetsiz Sayısı", + "field_label_csat_neutral_count": "CSAT Nötr Sayısı", + "field_label_csat_satisfied_count": "CSAT Memnun Sayısı", + "field_label_csat_score": "CSAT Puanı", "field_label_detractor_count": "Eleştirmen Sayısı", - "field_label_emotion": "Duygu", "field_label_field_type": "Alan Türü", + "field_label_language": "Dil", + "field_label_nps_average": "NPS Ortalaması", "field_label_nps_score": "NPS Puanı", - "field_label_nps_value": "NPS Değeri", "field_label_passive_count": "Pasif Sayısı", "field_label_promoter_count": "Tavsiye Eden Sayısı", + "field_label_question": "Soru", + "field_label_question_group": "Soru Grubu", "field_label_response_id": "Yanıt Kimliği", - "field_label_sentiment": "Duygu Durumu", "field_label_source_name": "Kaynak Adı", "field_label_source_type": "Kaynak Türü", - "field_label_topic": "Konu", + "field_label_unique_respondents": "Benzersiz Katılımcılar", + "field_label_unique_responses": "Benzersiz Yanıtlar", + "field_label_updated_at": "Güncellenme Tarihi", "field_label_user_identifier": "Kullanıcı Tanımlayıcısı", + "field_label_value_boolean": "Değer (Boolean)", + "field_label_value_date": "Değer (Tarih)", + "field_label_value_number": "Değer (Sayı)", + "field_label_value_text": "Değer (Metin)", "filter_data": "Verileri filtrele", "filters": "Filtreler", "filters_toggle_description": "Yalnızca aşağıdaki koşulları karşılayan verileri dahil et.", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 26299b4a3e..df41f43d42 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "图表保存失败", "field": "字段", - "field_label_average_score": "平均分", + "field_label_ces_average": "CES 平均值", + "field_label_ces_count": "CES 数量", "field_label_collected_at": "收集时间", "field_label_count": "数量", + "field_label_created_at": "创建时间", + "field_label_csat_average": "CSAT 平均值", + "field_label_csat_count": "CSAT 数量", + "field_label_csat_dissatisfied_count": "CSAT 不满意数量", + "field_label_csat_neutral_count": "CSAT 中立数量", + "field_label_csat_satisfied_count": "CSAT 满意数量", + "field_label_csat_score": "CSAT 得分", "field_label_detractor_count": "贬损者数量", - "field_label_emotion": "情感", "field_label_field_type": "字段类型", + "field_label_language": "语言", + "field_label_nps_average": "NPS 平均值", "field_label_nps_score": "NPS 得分", - "field_label_nps_value": "NPS 值", "field_label_passive_count": "中立者数量", "field_label_promoter_count": "推荐者数量", + "field_label_question": "问题", + "field_label_question_group": "问题组", "field_label_response_id": "响应 ID", - "field_label_sentiment": "情绪", "field_label_source_name": "来源名称", "field_label_source_type": "来源类型", - "field_label_topic": "主题", + "field_label_unique_respondents": "独立受访者", + "field_label_unique_responses": "独立回复", + "field_label_updated_at": "更新时间", "field_label_user_identifier": "用户标识", + "field_label_value_boolean": "值(布尔型)", + "field_label_value_date": "值(日期)", + "field_label_value_number": "数值", + "field_label_value_text": "文本值", "filter_data": "筛选数据", "filters": "筛选条件", "filters_toggle_description": "仅包含符合以下条件的数据。", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 89aef27d7d..476489c1a0 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -1730,22 +1730,37 @@ "failed_to_load_dashboards": "Failed to load dashboards", "failed_to_save_chart": "儲存圖表失敗", "field": "欄位", - "field_label_average_score": "平均分數", + "field_label_ces_average": "CES 平均值", + "field_label_ces_count": "CES 數量", "field_label_collected_at": "收集時間", "field_label_count": "數量", + "field_label_created_at": "建立時間", + "field_label_csat_average": "CSAT 平均值", + "field_label_csat_count": "CSAT 數量", + "field_label_csat_dissatisfied_count": "CSAT 不滿意數量", + "field_label_csat_neutral_count": "CSAT 中立數量", + "field_label_csat_satisfied_count": "CSAT 滿意數量", + "field_label_csat_score": "CSAT 分數", "field_label_detractor_count": "批評者數量", - "field_label_emotion": "情緒", "field_label_field_type": "欄位類型", + "field_label_language": "語言", + "field_label_nps_average": "NPS 平均值", "field_label_nps_score": "NPS 分數", - "field_label_nps_value": "NPS 值", "field_label_passive_count": "中立者數量", "field_label_promoter_count": "推廣者數量", + "field_label_question": "問題", + "field_label_question_group": "問題群組", "field_label_response_id": "回應 ID", - "field_label_sentiment": "情感", "field_label_source_name": "來源名稱", "field_label_source_type": "來源類型", - "field_label_topic": "主題", + "field_label_unique_respondents": "不重複受訪者", + "field_label_unique_responses": "不重複回應", + "field_label_updated_at": "更新時間", "field_label_user_identifier": "使用者識別碼", + "field_label_value_boolean": "值(布林)", + "field_label_value_date": "值(日期)", + "field_label_value_number": "數值", + "field_label_value_text": "文字值", "filter_data": "篩選資料", "filters": "篩選條件", "filters_toggle_description": "只包含符合下列條件的資料。", diff --git a/apps/web/modules/ee/analysis/api/lib/cube-client.test.ts b/apps/web/modules/ee/analysis/api/lib/cube-client.test.ts index 6c6e97d51f..dda1b15c00 100644 --- a/apps/web/modules/ee/analysis/api/lib/cube-client.test.ts +++ b/apps/web/modules/ee/analysis/api/lib/cube-client.test.ts @@ -187,7 +187,7 @@ describe("executeTenantScopedQuery", () => { ...scopedInput, query: { measures: ["FeedbackRecords.count"], - filters: [{ member: "FeedbackRecords.sentiment", operator: "equals", values: ["secret-value"] }], + filters: [{ member: "FeedbackRecords.sourceType", operator: "equals", values: ["secret-value"] }], }, }); @@ -197,7 +197,7 @@ describe("executeTenantScopedQuery", () => { targetType: "cubeQuery", newObject: expect.objectContaining({ query: expect.objectContaining({ - filterMembers: ["FeedbackRecords.sentiment"], + filterMembers: ["FeedbackRecords.sourceType"], filterCount: 1, }), }), diff --git a/apps/web/modules/ee/analysis/api/lib/cube-query-rewrite.test.ts b/apps/web/modules/ee/analysis/api/lib/cube-query-rewrite.test.ts index d4057cbdc9..d14a0bb202 100644 --- a/apps/web/modules/ee/analysis/api/lib/cube-query-rewrite.test.ts +++ b/apps/web/modules/ee/analysis/api/lib/cube-query-rewrite.test.ts @@ -107,18 +107,6 @@ describe("cube queryRewrite", () => { ).toThrow(/tenant filters are enforced by Cube/); }); - test("rejects caller-supplied TopicsUnnested tenant filters", () => { - expect(() => - queryRewrite( - { - measures: ["TopicsUnnested.count"], - filters: [{ member: "TopicsUnnested.tenantId", operator: "equals", values: ["workspace-2"] }], - }, - { securityContext } - ) - ).toThrow(/tenant filters are enforced by Cube/); - }); - test("logs sanitized failure audit metadata for rejected tenant filters", () => { expect(() => queryRewrite( @@ -169,7 +157,7 @@ describe("cube queryRewrite", () => { filters: [ { or: [ - { member: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }, + { member: "FeedbackRecords.sourceType", operator: "equals", values: ["positive"] }, { member: "FeedbackRecords.tenantId", operator: "equals", values: ["workspace-2"] }, ], }, @@ -207,36 +195,23 @@ describe("cube queryRewrite", () => { test("appends the mandatory tenant filter from security context", () => { const query = { measures: ["FeedbackRecords.count"], - filters: [{ member: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }], + filters: [{ member: "FeedbackRecords.sourceType", operator: "equals", values: ["positive"] }], }; const rewrittenQuery = queryRewrite(query, { securityContext }); expect(rewrittenQuery.filters).toEqual([ - { member: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }, + { member: "FeedbackRecords.sourceType", operator: "equals", values: ["positive"] }, { member: "FeedbackRecords.tenantId", operator: "equals", values: ["frd-1"] }, ]); expect(query.filters).toHaveLength(1); }); - test("appends only the TopicsUnnested tenant filter for TopicsUnnested queries", () => { - const query = { - measures: ["TopicsUnnested.count"], - dimensions: ["TopicsUnnested.topic"], - }; - - const rewrittenQuery = queryRewrite(query, { securityContext }); - - expect(rewrittenQuery.filters).toEqual([ - { member: "TopicsUnnested.tenantId", operator: "equals", values: ["frd-1"] }, - ]); - }); - test("logs sanitized Cube audit metadata without raw filter values", () => { queryRewrite( { measures: ["FeedbackRecords.count"], - filters: [{ member: "FeedbackRecords.sentiment", operator: "equals", values: ["secret-value"] }], + filters: [{ member: "FeedbackRecords.sourceType", operator: "equals", values: ["secret-value"] }], }, { securityContext } ); @@ -256,7 +231,6 @@ describe("cube queryRewrite", () => { source: "charts.executeQueryAction", }); expect(parsed.members).toContain("FeedbackRecords.tenantId"); - expect(parsed.members).not.toContain("TopicsUnnested.tenantId"); expect(logPayload).not.toContain("secret-value"); }); }); diff --git a/apps/web/modules/ee/analysis/api/lib/cube-query.test.ts b/apps/web/modules/ee/analysis/api/lib/cube-query.test.ts index e891464c44..e361145667 100644 --- a/apps/web/modules/ee/analysis/api/lib/cube-query.test.ts +++ b/apps/web/modules/ee/analysis/api/lib/cube-query.test.ts @@ -10,7 +10,7 @@ describe("cube-query", () => { expect(() => validateCubeQueryMembers({ measures: ["FeedbackRecords.count"], - dimensions: ["FeedbackRecords.sentiment"], + dimensions: ["FeedbackRecords.sourceType"], timeDimensions: [{ dimension: "FeedbackRecords.collectedAt" }], filters: [{ member: "FeedbackRecords.sourceType", operator: "equals", values: ["survey"] }], order: { "FeedbackRecords.collectedAt": "desc" }, @@ -18,10 +18,6 @@ describe("cube-query", () => { ).not.toThrow(); }); - test("allows TopicsUnnested dimensions from joined cube", () => { - expect(() => validateCubeQueryMembers({ dimensions: ["TopicsUnnested.topic"] })).not.toThrow(); - }); - test("throws for invalid members across query sections", () => { expect(() => validateCubeQueryMembers({ @@ -57,7 +53,7 @@ describe("cube-query", () => { filters: [ { or: [ - { member: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }, + { member: "FeedbackRecords.sourceType", operator: "equals", values: ["positive"] }, { and: [{ member: "FeedbackRecords.tenantId", operator: "equals", values: ["workspace-2"] }], }, @@ -90,7 +86,7 @@ describe("cube-query", () => { expect(() => validateCubeQueryMembers({ measures: ["FeedbackRecords.count", null], - dimensions: [{ member: "FeedbackRecords.sentiment" }], + dimensions: [{ member: "FeedbackRecords.sourceType" }], segments: [0], timeDimensions: [null, { dimension: null }], filters: [null, { member: { name: "FeedbackRecords.sourceType" } }, { and: [0] }, { or: "bad" }], @@ -103,7 +99,7 @@ describe("cube-query", () => { test("summarizes query members without raw filter values", () => { const summary = getCubeQueryAuditSummary({ measures: ["FeedbackRecords.count"], - dimensions: ["FeedbackRecords.sentiment"], + dimensions: ["FeedbackRecords.sourceType"], filters: [{ member: "FeedbackRecords.sourceType", operator: "equals", values: ["secret-value"] }], order: [["FeedbackRecords.collectedAt", "desc"]], limit: 50, @@ -111,7 +107,7 @@ describe("cube-query", () => { expect(summary).toEqual({ measures: ["FeedbackRecords.count"], - dimensions: ["FeedbackRecords.sentiment"], + dimensions: ["FeedbackRecords.sourceType"], segments: [], timeDimensions: [], filterMembers: ["FeedbackRecords.sourceType"], @@ -125,12 +121,12 @@ describe("cube-query", () => { test("summarizes only valid member names from malformed query shapes", () => { const summary = getCubeQueryAuditSummary({ measures: ["FeedbackRecords.count", null], - dimensions: [{ member: "FeedbackRecords.sentiment" }], + dimensions: [{ member: "FeedbackRecords.sourceType" }], timeDimensions: [null, { dimension: "FeedbackRecords.collectedAt" }], filters: [ null, { member: "FeedbackRecords.sourceType", operator: "equals", values: ["secret-value"] }, - { and: [0, { member: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }] }, + { and: [0, { member: "FeedbackRecords.sourceType", operator: "equals", values: ["positive"] }] }, ], order: [ [null, "asc"], @@ -143,7 +139,7 @@ describe("cube-query", () => { dimensions: [], segments: [], timeDimensions: ["FeedbackRecords.collectedAt"], - filterMembers: ["FeedbackRecords.sentiment", "FeedbackRecords.sourceType"], + filterMembers: ["FeedbackRecords.sourceType"], filterCount: 2, orderMembers: ["FeedbackRecords.collectedAt"], }); diff --git a/apps/web/modules/ee/analysis/api/lib/cube-query.ts b/apps/web/modules/ee/analysis/api/lib/cube-query.ts index 1661c2f48b..56f56d34bd 100644 --- a/apps/web/modules/ee/analysis/api/lib/cube-query.ts +++ b/apps/web/modules/ee/analysis/api/lib/cube-query.ts @@ -3,7 +3,7 @@ import type { TChartQuery } from "@formbricks/types/analysis"; export const TENANT_MEMBER = "FeedbackRecords.tenantId"; -const ALLOWED_CUBE_PREFIXES = ["FeedbackRecords.", "TopicsUnnested."]; +const ALLOWED_CUBE_PREFIXES = ["FeedbackRecords."]; const INVALID_MEMBER_REFERENCE = "invalid member reference"; type TQueryAuditSummary = { @@ -310,9 +310,9 @@ export const validateCubeQueryMembers = (query: TChartQuery): void => { if (result.invalidMembers.length > 0) { throw new Error( - `Invalid query members (must start with FeedbackRecords. or TopicsUnnested.): ${uniqueSorted( - result.invalidMembers - ).join(", ")}` + `Invalid query members (must start with FeedbackRecords.): ${uniqueSorted(result.invalidMembers).join( + ", " + )}` ); } }; diff --git a/apps/web/modules/ee/analysis/charts/actions.test.ts b/apps/web/modules/ee/analysis/charts/actions.test.ts index a729b7b475..7bdbcc04c9 100644 --- a/apps/web/modules/ee/analysis/charts/actions.test.ts +++ b/apps/web/modules/ee/analysis/charts/actions.test.ts @@ -175,7 +175,7 @@ describe("chart Cube actions", () => { mocks.generateText.mockResolvedValue({ output: { measures: ["FeedbackRecords.count"], - dimensions: ["FeedbackRecords.sentiment"], + dimensions: ["FeedbackRecords.sourceType"], timeDimensions: null, chartType: "bar", filters: null, @@ -198,7 +198,7 @@ describe("chart Cube actions", () => { expect(mocks.executeTenantScopedQuery).toHaveBeenCalledWith({ query: { measures: ["FeedbackRecords.count"], - dimensions: ["FeedbackRecords.sentiment"], + dimensions: ["FeedbackRecords.sourceType"], }, feedbackDirectoryId: "frd-1", workspaceId: "workspace-1", diff --git a/apps/web/modules/ee/analysis/charts/actions.ts b/apps/web/modules/ee/analysis/charts/actions.ts index 62393308c7..4b31d8bbe2 100644 --- a/apps/web/modules/ee/analysis/charts/actions.ts +++ b/apps/web/modules/ee/analysis/charts/actions.ts @@ -21,6 +21,11 @@ import { } from "@/modules/ee/analysis/charts/lib/charts"; import { checkFeedbackDirectoryAccess, checkWorkspaceAccess } from "@/modules/ee/analysis/lib/access"; import { generateSchemaContext } from "@/modules/ee/analysis/lib/ai-schema-context"; +import { + FEEDBACK_DIMENSION_IDS, + FEEDBACK_MEASURE_IDS, + FEEDBACK_TIME_DIMENSION_IDS, +} from "@/modules/ee/analysis/lib/schema-definition"; import { ZChartCreateInput, ZChartType, ZChartUpdateInput } from "@/modules/ee/analysis/types/analysis"; import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler"; import { getIsDashboardsEnabled } from "@/modules/ee/license-check/lib/utils"; @@ -287,13 +292,25 @@ export const executeQueryAction = authenticatedActionClient const CUBE_NAME = "FeedbackRecords"; +const toEnumTuple = (values: readonly string[]): [string, ...string[]] => { + if (values.length === 0) { + throw new Error("AI query schema requires at least one allowed id"); + } + return [values[0], ...values.slice(1)]; +}; + +const ZMeasureId = z.enum(toEnumTuple(FEEDBACK_MEASURE_IDS)); +const ZDimensionId = z.enum(toEnumTuple(FEEDBACK_DIMENSION_IDS)); +const ZTimeDimensionId = z.enum(toEnumTuple(FEEDBACK_TIME_DIMENSION_IDS)); +const ZFilterMemberId = z.enum(toEnumTuple([...FEEDBACK_MEASURE_IDS, ...FEEDBACK_DIMENSION_IDS])); + const ZGenerateAIQueryResponse = z.object({ - measures: z.array(z.string()), - dimensions: z.array(z.string()).nullable(), + measures: z.array(ZMeasureId), + dimensions: z.array(ZDimensionId).nullable(), timeDimensions: z .array( z.object({ - dimension: z.string(), + dimension: ZTimeDimensionId, granularity: z.enum(["hour", "day", "week", "month", "quarter", "year"]).nullable(), dateRange: z.string().nullable(), }) @@ -303,7 +320,7 @@ const ZGenerateAIQueryResponse = z.object({ filters: z .array( z.object({ - member: z.string(), + member: ZFilterMemberId, operator: z.enum([ "equals", "notEquals", @@ -364,6 +381,7 @@ export const generateAIChartAction = authenticatedActionClient output: Output.object({ schema: ZGenerateAIQueryResponse }), system: schemaContext, prompt: `User request: "${parsedInput.prompt}"`, + temperature: 0, }); const measures = output.measures.length > 0 ? output.measures : [`${CUBE_NAME}.count`]; diff --git a/apps/web/modules/ee/analysis/lib/ai-schema-context.ts b/apps/web/modules/ee/analysis/lib/ai-schema-context.ts index b76776e92f..67aef95f11 100644 --- a/apps/web/modules/ee/analysis/lib/ai-schema-context.ts +++ b/apps/web/modules/ee/analysis/lib/ai-schema-context.ts @@ -52,7 +52,7 @@ ${operatorsText} ## Guidelines - Always include at least one measure. If unspecified, default to \`${CUBE_NAME}.count\`. -- Use dimension IDs exactly as shown (e.g. \`FeedbackRecords.sentiment\`, \`FeedbackRecords.collectedAt\`). +- Use dimension IDs exactly as shown (e.g. \`FeedbackRecords.sourceType\`, \`FeedbackRecords.collectedAt\`). - For time-based filtering (date range only, no time grouping): add a timeDimension with dimension \`${CUBE_NAME}.collectedAt\` and dateRange. Do NOT include granularity (default is None / filter only). - For time-series or trend questions (e.g. "over time", "by day", "weekly", "monthly"): add a timeDimension with dimension, granularity (hour/day/week/month/quarter/year), and dateRange. - Choose the most appropriate chart type: bar, line, area, pie, or big_number (for single-number queries). diff --git a/apps/web/modules/ee/analysis/lib/query-builder.test.ts b/apps/web/modules/ee/analysis/lib/query-builder.test.ts index 61f26fe7d1..8f1b30897d 100644 --- a/apps/web/modules/ee/analysis/lib/query-builder.test.ts +++ b/apps/web/modules/ee/analysis/lib/query-builder.test.ts @@ -22,13 +22,13 @@ describe("query-builder", () => { test("adds dimensions when present", () => { const config: ChartBuilderState = { selectedMeasures: ["FeedbackRecords.count"], - selectedDimensions: ["FeedbackRecords.sentiment"], + selectedDimensions: ["FeedbackRecords.userId"], filters: [], filterLogic: "and", timeDimension: null, }; const query = buildCubeQuery(config); - expect(query.dimensions).toEqual(["FeedbackRecords.sentiment"]); + expect(query.dimensions).toEqual(["FeedbackRecords.userId"]); }); test("adds time dimension with string dateRange", () => { @@ -93,7 +93,7 @@ describe("query-builder", () => { selectedMeasures: ["FeedbackRecords.count"], selectedDimensions: [], filters: [ - { id: "f1", field: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }, + { id: "f1", field: "FeedbackRecords.userId", operator: "equals", values: ["positive"] }, { id: "f2", field: "FeedbackRecords.sourceType", operator: "set", values: null }, ], filterLogic: "and", @@ -101,7 +101,7 @@ describe("query-builder", () => { }; const query = buildCubeQuery(config); expect(query.filters).toEqual([ - { member: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }, + { member: "FeedbackRecords.userId", operator: "equals", values: ["positive"] }, { member: "FeedbackRecords.sourceType", operator: "set" }, ]); }); @@ -110,14 +110,14 @@ describe("query-builder", () => { const config: ChartBuilderState = { selectedMeasures: ["FeedbackRecords.count"], selectedDimensions: [], - filters: [{ id: "f1", field: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }], + filters: [{ id: "f1", field: "FeedbackRecords.userId", operator: "equals", values: ["positive"] }], filterLogic: "or", timeDimension: null, }; const query = buildCubeQuery(config); expect(query.filters).toEqual([ { - or: [{ member: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }], + or: [{ member: "FeedbackRecords.userId", operator: "equals", values: ["positive"] }], }, ]); }); @@ -136,12 +136,12 @@ describe("query-builder", () => { test("parses AND member filters", () => { const query = { measures: ["FeedbackRecords.count"], - filters: [{ member: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }], + filters: [{ member: "FeedbackRecords.userId", operator: "equals", values: ["positive"] }], }; const state = parseQueryToState(query); expect(state.filterLogic).toBe("and"); expect(state.filters?.map(({ field, operator, values }) => ({ field, operator, values }))).toEqual([ - { field: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }, + { field: "FeedbackRecords.userId", operator: "equals", values: ["positive"] }, ]); }); @@ -150,14 +150,14 @@ describe("query-builder", () => { measures: ["FeedbackRecords.count"], filters: [ { - or: [{ member: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }], + or: [{ member: "FeedbackRecords.userId", operator: "equals", values: ["positive"] }], }, ], }; const state = parseQueryToState(query); expect(state.filterLogic).toBe("or"); expect(state.filters?.map(({ field, operator, values }) => ({ field, operator, values }))).toEqual([ - { field: "FeedbackRecords.sentiment", operator: "equals", values: ["positive"] }, + { field: "FeedbackRecords.userId", operator: "equals", values: ["positive"] }, ]); }); @@ -202,7 +202,7 @@ describe("query-builder", () => { test("buildCubeQuery then parseQueryToState restores state", () => { const config: ChartBuilderState = { selectedMeasures: ["FeedbackRecords.count"], - selectedDimensions: ["FeedbackRecords.sentiment"], + selectedDimensions: ["FeedbackRecords.userId"], filters: [{ id: "f1", field: "FeedbackRecords.sourceType", operator: "equals", values: ["survey"] }], filterLogic: "and", timeDimension: { diff --git a/apps/web/modules/ee/analysis/lib/schema-definition.test.ts b/apps/web/modules/ee/analysis/lib/schema-definition.test.ts index 8da70e4735..dd1d0d8bfc 100644 --- a/apps/web/modules/ee/analysis/lib/schema-definition.test.ts +++ b/apps/web/modules/ee/analysis/lib/schema-definition.test.ts @@ -32,9 +32,9 @@ describe("schema-definition", () => { describe("getFieldById", () => { test("returns dimension by id", () => { - const field = getFieldById("FeedbackRecords.sentiment"); + const field = getFieldById("FeedbackRecords.sourceType"); expect(field).toBeDefined(); - expect(field?.label).toBe("Sentiment"); + expect(field?.label).toBe("Source Type"); expect(field?.type).toBe("string"); }); @@ -56,7 +56,7 @@ describe("schema-definition", () => { }); test("returns field label for known dimension/measure", () => { - expect(formatCubeColumnHeader("FeedbackRecords.sentiment")).toBe("Sentiment"); + expect(formatCubeColumnHeader("FeedbackRecords.sourceType")).toBe("Source Type"); expect(formatCubeColumnHeader("FeedbackRecords.count")).toBe("Count"); }); @@ -74,5 +74,25 @@ describe("schema-definition", () => { expect(FEEDBACK_FIELDS.dimensions.length).toBeGreaterThan(0); expect(FEEDBACK_FIELDS.measures.length).toBeGreaterThan(0); }); + + test("exposes CSAT, CES, NPS and universal measures", () => { + const ids = FEEDBACK_FIELDS.measures.map((m) => m.id); + expect(ids).toEqual( + expect.arrayContaining([ + "FeedbackRecords.count", + "FeedbackRecords.uniqueRespondents", + "FeedbackRecords.uniqueResponses", + "FeedbackRecords.npsScore", + "FeedbackRecords.npsAverage", + "FeedbackRecords.csatScore", + "FeedbackRecords.csatAverage", + "FeedbackRecords.csatSatisfiedCount", + "FeedbackRecords.csatCount", + "FeedbackRecords.cesAverage", + "FeedbackRecords.cesCount", + ]) + ); + expect(ids).not.toContain("FeedbackRecords.averageScore"); + }); }); }); diff --git a/apps/web/modules/ee/analysis/lib/schema-definition.ts b/apps/web/modules/ee/analysis/lib/schema-definition.ts index bd61acf7fc..1d0e4512b7 100644 --- a/apps/web/modules/ee/analysis/lib/schema-definition.ts +++ b/apps/web/modules/ee/analysis/lib/schema-definition.ts @@ -7,7 +7,7 @@ import type { TFunction } from "i18next"; export interface FieldDefinition { id: string; label: string; - type: "string" | "number" | "time"; + type: "string" | "number" | "time" | "boolean"; description?: string; } @@ -20,12 +20,6 @@ export interface MeasureDefinition { export const FEEDBACK_FIELDS = { dimensions: [ - { - id: "FeedbackRecords.sentiment", - label: "Sentiment", - type: "string", - description: "Sentiment extracted from feedback", - }, { id: "FeedbackRecords.sourceType", label: "Source Type", @@ -45,10 +39,22 @@ export const FEEDBACK_FIELDS = { description: "Type of feedback field (e.g., nps, text, rating)", }, { - id: "FeedbackRecords.emotion", - label: "Emotion", + id: "FeedbackRecords.fieldLabel", + label: "Question", type: "string", - description: "Emotion extracted from metadata JSONB field", + description: "Human-readable label of the question/field", + }, + { + id: "FeedbackRecords.fieldGroupLabel", + label: "Question Group", + type: "string", + description: "Label of the parent composite question for matrix/ranking rows", + }, + { + id: "FeedbackRecords.language", + label: "Language", + type: "string", + description: 'Response language code (e.g., "en", "de")', }, { id: "FeedbackRecords.userId", @@ -63,10 +69,30 @@ export const FEEDBACK_FIELDS = { description: "Unique identifier linking related feedback records", }, { - id: "FeedbackRecords.npsValue", - label: "NPS Value", + id: "FeedbackRecords.valueNumber", + label: "Value (Number)", type: "number", - description: "Raw NPS score value (0-10)", + description: + "Numeric answer value (NPS 0-10, CSAT 1-5, CES 1-5 or 1-7, rating, number). Pair with a fieldType filter to keep scales consistent.", + }, + { + id: "FeedbackRecords.valueText", + label: "Value (Text)", + type: "string", + description: + "Text answer value (open text, or the label of a multiple-choice/categorical answer). Pair with a fieldType filter to keep types consistent.", + }, + { + id: "FeedbackRecords.valueBoolean", + label: "Value (Boolean)", + type: "boolean", + description: "Boolean answer value (yes/no). Pair with a fieldType filter.", + }, + { + id: "FeedbackRecords.valueDate", + label: "Value (Date)", + type: "time", + description: "Date answer value. Pair with a fieldType filter.", }, { id: "FeedbackRecords.collectedAt", @@ -75,10 +101,16 @@ export const FEEDBACK_FIELDS = { description: "Timestamp when the feedback was collected", }, { - id: "TopicsUnnested.topic", - label: "Topic", - type: "string", - description: "Individual topic from the topics array", + id: "FeedbackRecords.createdAt", + label: "Created At", + type: "time", + description: "Timestamp when the feedback record was created in Hub", + }, + { + id: "FeedbackRecords.updatedAt", + label: "Updated At", + type: "time", + description: "Timestamp when the feedback record was last updated in Hub", }, ] as FieldDefinition[], measures: [ @@ -89,38 +121,106 @@ export const FEEDBACK_FIELDS = { description: "Total number of feedback responses", }, { - id: "FeedbackRecords.promoterCount", - label: "Promoter Count", - type: "count", - description: "Number of promoters (NPS score 9-10)", + id: "FeedbackRecords.uniqueRespondents", + label: "Unique Respondents", + type: "number", + description: "Number of unique users who provided feedback", }, { - id: "FeedbackRecords.detractorCount", - label: "Detractor Count", - type: "count", - description: "Number of detractors (NPS score 0-6)", - }, - { - id: "FeedbackRecords.passiveCount", - label: "Passive Count", - type: "count", - description: "Number of passives (NPS score 7-8)", + id: "FeedbackRecords.uniqueResponses", + label: "Unique Responses", + type: "number", + description: "Number of unique survey submissions", }, { id: "FeedbackRecords.npsScore", label: "NPS Score", type: "number", - description: "Net Promoter Score: ((Promoters - Detractors) / Total) * 100", + description: "Net Promoter Score: ((Promoters - Detractors) / Total NPS responses) * 100", }, { - id: "FeedbackRecords.averageScore", - label: "Average Score", + id: "FeedbackRecords.npsAverage", + label: "NPS Average", type: "number", - description: "Average NPS score", + description: "Average NPS rating (0-10)", + }, + { + id: "FeedbackRecords.promoterCount", + label: "Promoter Count", + type: "count", + description: "Number of NPS promoters (score 9-10)", + }, + { + id: "FeedbackRecords.passiveCount", + label: "Passive Count", + type: "count", + description: "Number of NPS passives (score 7-8)", + }, + { + id: "FeedbackRecords.detractorCount", + label: "Detractor Count", + type: "count", + description: "Number of NPS detractors (score 0-6)", + }, + { + id: "FeedbackRecords.csatScore", + label: "CSAT Score", + type: "number", + description: "CSAT Score: % of CSAT responses rated 4 or 5 (top-2-box on the 1-5 scale)", + }, + { + id: "FeedbackRecords.csatAverage", + label: "CSAT Average", + type: "number", + description: "Average CSAT rating (1-5)", + }, + { + id: "FeedbackRecords.csatSatisfiedCount", + label: "CSAT Satisfied Count", + type: "count", + description: "Number of satisfied CSAT responses (top-2-box on the 1-5 scale)", + }, + { + id: "FeedbackRecords.csatDissatisfiedCount", + label: "CSAT Dissatisfied Count", + type: "count", + description: "Number of dissatisfied CSAT responses (bottom-2-box on the 1-5 scale)", + }, + { + id: "FeedbackRecords.csatNeutralCount", + label: "CSAT Neutral Count", + type: "count", + description: "Number of neutral CSAT responses (middle box on the 1-5 scale)", + }, + { + id: "FeedbackRecords.csatCount", + label: "CSAT Count", + type: "count", + description: "Number of CSAT responses", + }, + { + id: "FeedbackRecords.cesAverage", + label: "CES Average", + type: "number", + description: "Average CES rating (scale is 1-5 or 1-7 depending on the question)", + }, + { + id: "FeedbackRecords.cesCount", + label: "CES Count", + type: "count", + description: "Number of CES responses", }, ] as MeasureDefinition[], }; +export const FEEDBACK_MEASURE_IDS: string[] = FEEDBACK_FIELDS.measures.map((m) => m.id); + +export const FEEDBACK_DIMENSION_IDS: string[] = FEEDBACK_FIELDS.dimensions.map((d) => d.id); + +export const FEEDBACK_TIME_DIMENSION_IDS: string[] = FEEDBACK_FIELDS.dimensions + .filter((d) => d.type === "time") + .map((d) => d.id); + export type FilterOperator = | "equals" | "notEquals" @@ -137,6 +237,7 @@ export const FILTER_OPERATORS: Record = { string: ["equals", "notEquals", "contains", "notContains", "set", "notSet"], number: ["equals", "notEquals", "gt", "gte", "lt", "lte", "set", "notSet"], time: ["equals", "notEquals", "gt", "gte", "lt", "lte", "set", "notSet"], + boolean: ["equals", "notEquals", "set", "notSet"], }; export const TIME_GRANULARITIES = ["hour", "day", "week", "month", "quarter", "year"] as const; @@ -166,7 +267,7 @@ export const DATE_PRESETS = [ /** * Get filter operators for a given field type. */ -export function getFilterOperatorsForType(type: "string" | "number" | "time"): FilterOperator[] { +export function getFilterOperatorsForType(type: "string" | "number" | "time" | "boolean"): FilterOperator[] { return FILTER_OPERATORS[type] || FILTER_OPERATORS.string; } @@ -184,22 +285,39 @@ export function getFieldById(id: string): FieldDefinition | MeasureDefinition | */ export function getTranslatedFieldLabel(id: string, t: TFunction): string { const labels: Record = { - "FeedbackRecords.sentiment": t("workspace.analysis.charts.field_label_sentiment"), "FeedbackRecords.sourceType": t("workspace.analysis.charts.field_label_source_type"), "FeedbackRecords.sourceName": t("workspace.analysis.charts.field_label_source_name"), "FeedbackRecords.fieldType": t("workspace.analysis.charts.field_label_field_type"), - "FeedbackRecords.emotion": t("workspace.analysis.charts.field_label_emotion"), + "FeedbackRecords.fieldLabel": t("workspace.analysis.charts.field_label_question"), + "FeedbackRecords.fieldGroupLabel": t("workspace.analysis.charts.field_label_question_group"), + "FeedbackRecords.language": t("workspace.analysis.charts.field_label_language"), "FeedbackRecords.userId": t("workspace.analysis.charts.field_label_user_identifier"), "FeedbackRecords.responseId": t("workspace.analysis.charts.field_label_response_id"), - "FeedbackRecords.npsValue": t("workspace.analysis.charts.field_label_nps_value"), + "FeedbackRecords.valueNumber": t("workspace.analysis.charts.field_label_value_number"), + "FeedbackRecords.valueText": t("workspace.analysis.charts.field_label_value_text"), + "FeedbackRecords.valueBoolean": t("workspace.analysis.charts.field_label_value_boolean"), + "FeedbackRecords.valueDate": t("workspace.analysis.charts.field_label_value_date"), "FeedbackRecords.collectedAt": t("workspace.analysis.charts.field_label_collected_at"), - "TopicsUnnested.topic": t("workspace.analysis.charts.field_label_topic"), + "FeedbackRecords.createdAt": t("workspace.analysis.charts.field_label_created_at"), + "FeedbackRecords.updatedAt": t("workspace.analysis.charts.field_label_updated_at"), "FeedbackRecords.count": t("workspace.analysis.charts.field_label_count"), - "FeedbackRecords.promoterCount": t("workspace.analysis.charts.field_label_promoter_count"), - "FeedbackRecords.detractorCount": t("workspace.analysis.charts.field_label_detractor_count"), - "FeedbackRecords.passiveCount": t("workspace.analysis.charts.field_label_passive_count"), + "FeedbackRecords.uniqueRespondents": t("workspace.analysis.charts.field_label_unique_respondents"), + "FeedbackRecords.uniqueResponses": t("workspace.analysis.charts.field_label_unique_responses"), "FeedbackRecords.npsScore": t("workspace.analysis.charts.field_label_nps_score"), - "FeedbackRecords.averageScore": t("workspace.analysis.charts.field_label_average_score"), + "FeedbackRecords.npsAverage": t("workspace.analysis.charts.field_label_nps_average"), + "FeedbackRecords.promoterCount": t("workspace.analysis.charts.field_label_promoter_count"), + "FeedbackRecords.passiveCount": t("workspace.analysis.charts.field_label_passive_count"), + "FeedbackRecords.detractorCount": t("workspace.analysis.charts.field_label_detractor_count"), + "FeedbackRecords.csatScore": t("workspace.analysis.charts.field_label_csat_score"), + "FeedbackRecords.csatAverage": t("workspace.analysis.charts.field_label_csat_average"), + "FeedbackRecords.csatSatisfiedCount": t("workspace.analysis.charts.field_label_csat_satisfied_count"), + "FeedbackRecords.csatDissatisfiedCount": t( + "workspace.analysis.charts.field_label_csat_dissatisfied_count" + ), + "FeedbackRecords.csatNeutralCount": t("workspace.analysis.charts.field_label_csat_neutral_count"), + "FeedbackRecords.csatCount": t("workspace.analysis.charts.field_label_csat_count"), + "FeedbackRecords.cesAverage": t("workspace.analysis.charts.field_label_ces_average"), + "FeedbackRecords.cesCount": t("workspace.analysis.charts.field_label_ces_count"), }; return labels[id] ?? getFieldById(id)?.label ?? id; } diff --git a/charts/formbricks/cube/cube.js b/charts/formbricks/cube/cube.js index fdb3447662..d9a664604f 100644 --- a/charts/formbricks/cube/cube.js +++ b/charts/formbricks/cube/cube.js @@ -1,6 +1,6 @@ /* eslint-env es2022 */ -const TENANT_MEMBERS = ["FeedbackRecords.tenantId", "TopicsUnnested.tenantId"]; +const TENANT_MEMBERS = ["FeedbackRecords.tenantId"]; const REQUIRED_SCOPE = "xm:cube:query"; function assertRequiredEnvironmentVariable(name) { diff --git a/charts/formbricks/cube/schema/FeedbackRecords.js b/charts/formbricks/cube/schema/FeedbackRecords.js index df8db5afa2..86184b90bf 100644 --- a/charts/formbricks/cube/schema/FeedbackRecords.js +++ b/charts/formbricks/cube/schema/FeedbackRecords.js @@ -1,6 +1,5 @@ // This schema maps to the `feedback_records` table owned by the Formbricks Hub Postgres. -// If the Hub changes column names, types, or the metadata JSONB shape (e.g. the `topics` array), -// this schema must be updated to match. +// If the Hub changes column names or types, this schema must be updated to match. cube(`FeedbackRecords`, { sql: `SELECT * FROM feedback_records`, @@ -60,12 +59,6 @@ cube(`FeedbackRecords`, { primaryKey: true, }, - sentiment: { - sql: `sentiment`, - type: `string`, - description: `Sentiment extracted from metadata JSONB field`, - }, - sourceType: { sql: `source_type`, type: `string`, @@ -108,65 +101,10 @@ cube(`FeedbackRecords`, { description: `Identifier of the user who provided feedback`, }, - emotion: { - sql: `emotion`, - type: `string`, - description: `Emotion extracted from metadata JSONB field`, - }, - tenantId: { sql: `tenant_id`, type: `string`, description: `Tenant ID linking to FeedbackDirectory`, }, }, - - joins: { - TopicsUnnested: { - sql: `${CUBE}.id = ${TopicsUnnested}.feedback_record_id`, - relationship: `hasMany`, - }, - }, -}); - -cube(`TopicsUnnested`, { - sql: ` - SELECT - fr.id as feedback_record_id, - fr.tenant_id, - topic_elem.topic - FROM feedback_records fr - CROSS JOIN LATERAL jsonb_array_elements_text(COALESCE(fr.metadata->'topics', '[]'::jsonb)) AS topic_elem(topic) - `, - - measures: { - count: { - type: `count`, - }, - }, - - dimensions: { - id: { - sql: `md5(feedback_record_id || '::' || topic)`, - type: `string`, - primaryKey: true, - }, - - feedbackRecordId: { - sql: `feedback_record_id`, - type: `string`, - }, - - tenantId: { - sql: `tenant_id`, - type: `string`, - description: `Tenant ID for row-level security scoping`, - }, - - topic: { - sql: `topic`, - type: `string`, - description: `Individual topic from the topics array`, - }, - }, }); diff --git a/docker/cube/cube.js b/docker/cube/cube.js index fdb3447662..d9a664604f 100644 --- a/docker/cube/cube.js +++ b/docker/cube/cube.js @@ -1,6 +1,6 @@ /* eslint-env es2022 */ -const TENANT_MEMBERS = ["FeedbackRecords.tenantId", "TopicsUnnested.tenantId"]; +const TENANT_MEMBERS = ["FeedbackRecords.tenantId"]; const REQUIRED_SCOPE = "xm:cube:query"; function assertRequiredEnvironmentVariable(name) { diff --git a/docker/cube/schema/FeedbackRecords.js b/docker/cube/schema/FeedbackRecords.js index 1ea1a00a53..763c6fe2cf 100644 --- a/docker/cube/schema/FeedbackRecords.js +++ b/docker/cube/schema/FeedbackRecords.js @@ -1,6 +1,5 @@ // This schema maps to the `feedback_records` table owned by the Formbricks Hub Postgres. -// If the Hub changes column names, types, or the metadata JSONB shape (e.g. the `topics` array), -// this schema must be updated to match. +// If the Hub changes column names or types, this schema must be updated to match. cube(`FeedbackRecords`, { sql: `SELECT * FROM feedback_records`, @@ -10,46 +9,120 @@ cube(`FeedbackRecords`, { description: `Total number of feedback responses`, }, + uniqueRespondents: { + type: `countDistinct`, + sql: `${CUBE}.user_id`, + description: `Number of unique users who provided feedback`, + }, + + uniqueResponses: { + type: `countDistinct`, + sql: `${CUBE}.submission_id`, + description: `Number of unique survey submissions (a submission can produce multiple feedback records)`, + }, + promoterCount: { type: `count`, - filters: [{ sql: `${CUBE}.value_number >= 9` }], - description: `Number of promoters (NPS score 9-10)`, + filters: [{ sql: `${CUBE}.field_type = 'nps' AND ${CUBE}.value_number >= 9` }], + description: `Number of NPS promoters (score 9-10)`, }, detractorCount: { type: `count`, - filters: [{ sql: `${CUBE}.value_number >= 0 AND ${CUBE}.value_number <= 6` }], - description: `Number of detractors (NPS score 0-6)`, + filters: [{ sql: `${CUBE}.field_type = 'nps' AND ${CUBE}.value_number BETWEEN 0 AND 6` }], + description: `Number of NPS detractors (score 0-6)`, }, passiveCount: { type: `count`, - filters: [{ sql: `${CUBE}.value_number >= 7 AND ${CUBE}.value_number <= 8` }], - description: `Number of passives (NPS score 7-8)`, + filters: [{ sql: `${CUBE}.field_type = 'nps' AND ${CUBE}.value_number BETWEEN 7 AND 8` }], + description: `Number of NPS passives (score 7-8)`, }, npsScore: { type: `number`, sql: ` CASE - WHEN COUNT(*) = 0 THEN 0 + WHEN COUNT(CASE WHEN ${CUBE}.field_type = 'nps' AND ${CUBE}.value_number IS NOT NULL THEN 1 END) = 0 THEN NULL ELSE ROUND( ( - (COUNT(CASE WHEN ${CUBE}.value_number >= 9 THEN 1 END)::numeric - - COUNT(CASE WHEN ${CUBE}.value_number >= 0 AND ${CUBE}.value_number <= 6 THEN 1 END)::numeric) - / COUNT(*)::numeric + (COUNT(CASE WHEN ${CUBE}.field_type = 'nps' AND ${CUBE}.value_number >= 9 THEN 1 END)::numeric - + COUNT(CASE WHEN ${CUBE}.field_type = 'nps' AND ${CUBE}.value_number BETWEEN 0 AND 6 THEN 1 END)::numeric) + / COUNT(CASE WHEN ${CUBE}.field_type = 'nps' AND ${CUBE}.value_number IS NOT NULL THEN 1 END)::numeric ) * 100, 2 ) END `, - description: `Net Promoter Score: ((Promoters - Detractors) / Total) * 100`, + description: `Net Promoter Score: ((Promoters - Detractors) / Answered NPS responses) * 100. NULL when there are no answered NPS responses.`, }, - averageScore: { + npsAverage: { type: `avg`, sql: `${CUBE}.value_number`, - description: `Average NPS score`, + filters: [{ sql: `${CUBE}.field_type = 'nps'` }], + description: `Average NPS rating (0-10)`, + }, + + csatCount: { + type: `count`, + filters: [{ sql: `${CUBE}.field_type = 'csat' AND ${CUBE}.value_number IS NOT NULL` }], + description: `Number of answered CSAT responses (dismissed responses excluded).`, + }, + + csatSatisfiedCount: { + type: `count`, + filters: [{ sql: `${CUBE}.field_type = 'csat' AND ${CUBE}.value_number >= 4` }], + description: `Number of satisfied CSAT responses (top-2-box on the 1-5 scale)`, + }, + + csatDissatisfiedCount: { + type: `count`, + filters: [{ sql: `${CUBE}.field_type = 'csat' AND ${CUBE}.value_number BETWEEN 1 AND 2` }], + description: `Number of dissatisfied CSAT responses (bottom-2-box on the 1-5 scale)`, + }, + + csatNeutralCount: { + type: `count`, + filters: [{ sql: `${CUBE}.field_type = 'csat' AND ${CUBE}.value_number = 3` }], + description: `Number of neutral CSAT responses (middle box on the 1-5 scale)`, + }, + + csatScore: { + type: `number`, + sql: ` + CASE + WHEN COUNT(CASE WHEN ${CUBE}.field_type = 'csat' AND ${CUBE}.value_number IS NOT NULL THEN 1 END) = 0 THEN NULL + ELSE ROUND( + ( + COUNT(CASE WHEN ${CUBE}.field_type = 'csat' AND ${CUBE}.value_number >= 4 THEN 1 END)::numeric + / COUNT(CASE WHEN ${CUBE}.field_type = 'csat' AND ${CUBE}.value_number IS NOT NULL THEN 1 END)::numeric + ) * 100, + 2 + ) + END + `, + description: `CSAT Score: % of answered CSAT responses rated 4 or 5 (top-2-box on the 1-5 scale). NULL when there are no answered CSAT responses.`, + }, + + csatAverage: { + type: `avg`, + sql: `${CUBE}.value_number`, + filters: [{ sql: `${CUBE}.field_type = 'csat'` }], + description: `Average CSAT rating (1-5)`, + }, + + cesCount: { + type: `count`, + filters: [{ sql: `${CUBE}.field_type = 'ces' AND ${CUBE}.value_number IS NOT NULL` }], + description: `Number of answered CES responses (dismissed responses excluded).`, + }, + + cesAverage: { + type: `avg`, + sql: `${CUBE}.value_number`, + filters: [{ sql: `${CUBE}.field_type = 'ces'` }], + description: `Average CES rating (scale is 1-5 or 1-7 depending on the question)`, }, }, @@ -60,12 +133,6 @@ cube(`FeedbackRecords`, { primaryKey: true, }, - sentiment: { - sql: `${CUBE}.metadata->>'sentiment'`, - type: `string`, - description: `Sentiment extracted from metadata JSONB field`, - }, - sourceType: { sql: `source_type`, type: `string`, @@ -84,16 +151,64 @@ cube(`FeedbackRecords`, { description: `Type of feedback field (e.g., nps, text, rating)`, }, + fieldLabel: { + sql: `field_label`, + type: `string`, + description: `Human-readable label of the question/field (e.g., "How satisfied are you with support?")`, + }, + + fieldGroupLabel: { + sql: `field_group_label`, + type: `string`, + description: `Label of the parent composite question for matrix/ranking rows`, + }, + + language: { + sql: `language`, + type: `string`, + description: `Response language code (e.g., "en", "de"). NULL when language is "default".`, + }, + collectedAt: { sql: `collected_at`, type: `time`, description: `Timestamp when the feedback was collected`, }, - npsValue: { + createdAt: { + sql: `created_at`, + type: `time`, + description: `Timestamp when the feedback record was created in Hub`, + }, + + updatedAt: { + sql: `updated_at`, + type: `time`, + description: `Timestamp when the feedback record was last updated in Hub`, + }, + + valueNumber: { sql: `value_number`, type: `number`, - description: `Raw NPS score value (0-10)`, + description: `Numeric answer value (NPS 0-10, CSAT 1-5, CES 1-5 or 1-7, rating, generic number). Pair with a fieldType filter to keep scales consistent.`, + }, + + valueText: { + sql: `value_text`, + type: `string`, + description: `Text answer value (open text, or the label of a multiple-choice / categorical answer). Pair with a fieldType filter to keep types consistent.`, + }, + + valueBoolean: { + sql: `value_boolean`, + type: `boolean`, + description: `Boolean answer value (yes/no questions). Pair with a fieldType filter.`, + }, + + valueDate: { + sql: `value_date`, + type: `time`, + description: `Date answer value (e.g., "preferred meeting date"). Pair with a fieldType filter.`, }, responseId: { @@ -108,65 +223,10 @@ cube(`FeedbackRecords`, { description: `Identifier of the user who provided feedback`, }, - emotion: { - sql: `${CUBE}.metadata->>'emotion'`, - type: `string`, - description: `Emotion extracted from metadata JSONB field`, - }, - tenantId: { sql: `tenant_id`, type: `string`, description: `Tenant ID linking to FeedbackDirectory`, }, }, - - joins: { - TopicsUnnested: { - sql: `${CUBE}.id = ${TopicsUnnested}.feedback_record_id`, - relationship: `hasMany`, - }, - }, -}); - -cube(`TopicsUnnested`, { - sql: ` - SELECT - fr.id as feedback_record_id, - fr.tenant_id, - topic_elem.topic - FROM feedback_records fr - CROSS JOIN LATERAL jsonb_array_elements_text(COALESCE(fr.metadata->'topics', '[]'::jsonb)) AS topic_elem(topic) - `, - - measures: { - count: { - type: `count`, - }, - }, - - dimensions: { - id: { - sql: `md5(feedback_record_id || '::' || topic)`, - type: `string`, - primaryKey: true, - }, - - feedbackRecordId: { - sql: `feedback_record_id`, - type: `string`, - }, - - tenantId: { - sql: `tenant_id`, - type: `string`, - description: `Tenant ID for row-level security scoping`, - }, - - topic: { - sql: `topic`, - type: `string`, - description: `Individual topic from the topics array`, - }, - }, });