Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: add surveys to querybook #1375

Merged
merged 4 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs_website/docs/integrations/add_surveys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
id: add_surveys
title: Add Surveys
sidebar_label: Add Surveys
---

Product surveys serve as an excellent tool to gather user feedback. Querybook supports several kinds of surveys out-of-the-box, including:

1. **Table search**: Users can indicate if the table search results matched their expectations.
2. **Table trust**: Users can rate their trust in the provided table metadata.
3. **Text to SQL**: Users can evaluate the quality of AI-generated SQL code.
4. **Query authoring**: Users can rate their experience of writing queries on Querybook.

Each of these surveys follows the same 1-5 rating format, complemented by an optional text field for additional comments.
By default, the surveys are disabled. If you wish to enable them, override the `querybook/config/querybook_public_config.yaml` file.

Below is an example of a setting that enables all surveys:

```yaml
survey:
global_response_cooldown: 2592000 # 30 days
global_trigger_cooldown: 600 # 10 minutes
global_max_per_week: 6
global_max_per_day: 3

surfaces:
- surface: table_search
max_per_week: 5
- surface: table_view
max_per_day: 4
- surface: text_to_sql
response_cooldown: 24000
- surface: query_authoring
```

To activate a survey for a specific surface, you need to include the relevant surface key under `surfaces`.
You can find out the list of all support surfaces in `SurveyTypeToQuestion` located under `querybook/webapp/const/survey.ts`.

There are 4 variables that you can configure either for eaceh individual surface or globally for surveys, they are:

- **response_cooldown**: Time (in seconds) the system waits before showing the same survey to a user who has already responded.
- **trigger_cooldown**: Waiting period before the same survey is shown to the same user.
- **max_per_week**: Maximum number of surveys shown to a user per week (per surface type).
- **max_per_day**: Daily limit for the number of surveys shown to a user (per surface type).
1 change: 1 addition & 0 deletions docs_website/sidebars.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"integrations/add_table_upload",
"integrations/add_event_logger",
"integrations/add_stats_logger",
"integrations/add_surveys",
"integrations/add_ai_assistant",
"integrations/customize_html",
"integrations/embedded_iframe"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "querybook",
"version": "3.28.3",
"version": "3.28.4",
"description": "A Big Data Webapp",
"private": true,
"scripts": {
Expand Down
16 changes: 16 additions & 0 deletions querybook/config/querybook_public_config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Public Configs are shared between backend and frontend and are not sensitive
# --------------- AI Assistant ---------------
ai_assistant:
enabled: false
Expand All @@ -13,3 +14,18 @@ ai_assistant:

table_vector_search:
enabled: false

survey:
global_response_cooldown: 2592000 # 30 days
global_trigger_cooldown: 600 # 10 minutes
global_max_per_week: 6
global_max_per_day: 3

surfaces: []

# Uncomment to enable survey on all surfaces
# surfaces:
# - surface: table_search
# - surface: table_view
# - surface: text_to_sql
# - surface: query_authoring
41 changes: 41 additions & 0 deletions querybook/migrations/versions/c00f08f16065_add_surveys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Add surveys

Revision ID: c00f08f16065
Revises: 4c70dae378f2
Create Date: 2023-11-20 22:40:36.139101

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = "c00f08f16065"
down_revision = "4c70dae378f2"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"survey",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.Column("uid", sa.Integer(), nullable=True),
sa.Column("rating", sa.Integer(), nullable=False),
sa.Column("comment", sa.String(length=5000), nullable=True),
sa.Column("surface", sa.String(length=255), nullable=False),
sa.Column("surface_metadata", sa.JSON(), nullable=False),
sa.ForeignKeyConstraint(["uid"], ["user.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("survey")
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions querybook/server/datasources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from . import event_log
from . import data_element
from . import comment
from . import survey

# Keep this at the end of imports to make sure the plugin APIs override the default ones
try:
Expand All @@ -42,4 +43,5 @@
event_log
data_element
comment
survey
api_plugin
27 changes: 27 additions & 0 deletions querybook/server/datasources/survey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from flask_login import current_user

from app.datasource import register
from logic import survey as logic


@register("/survey/", methods=["POST"])
def create_survey(
rating: int, surface: str, surface_metadata: dict[str, str], comment: str = None
):
return logic.create_survey(
uid=current_user.id,
rating=rating,
surface=surface,
surface_metadata=surface_metadata,
comment=comment,
)


@register("/survey/<int:survey_id>/", methods=["PUT"])
def update_survey(survey_id: int, rating: int = None, comment: str = None):
return logic.update_survey(
uid=current_user.id,
survey_id=survey_id,
rating=rating,
comment=comment,
)
52 changes: 52 additions & 0 deletions querybook/server/logic/survey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import datetime

from app.db import with_session
from models.survey import Survey


@with_session
def create_survey(
uid: int,
rating: int,
surface: str,
surface_metadata: dict[str, str] = {},
comment: str = None,
commit: bool = True,
session=None,
):
return Survey.create(
{
"uid": uid,
"rating": rating,
"surface": surface,
"surface_metadata": surface_metadata,
"comment": comment,
},
commit=commit,
session=session,
)


@with_session
def update_survey(
uid: int,
survey_id: int,
rating: int = None,
comment: str = None,
commit: bool = True,
session=None,
):
survey = Survey.get(id=survey_id, session=session)
assert survey.uid == uid, "User does not own this survey"

return Survey.update(
id=survey_id,
fields={
"rating": rating,
"comment": comment,
"updated_at": datetime.datetime.now(),
},
skip_if_value_none=True,
commit=commit,
session=session,
)
1 change: 1 addition & 0 deletions querybook/server/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
from .event_log import *
from .data_element import *
from .comment import *
from .survey import *
27 changes: 27 additions & 0 deletions querybook/server/models/survey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import sqlalchemy as sql

from app import db
from const.db import (
description_length,
name_length,
now,
)
from lib.sqlalchemy import CRUDMixin

Base = db.Base


class Survey(CRUDMixin, Base):
__tablename__ = "survey"

id = sql.Column(sql.Integer, primary_key=True)
created_at = sql.Column(sql.DateTime, default=now)
updated_at = sql.Column(sql.DateTime, default=now)

uid = sql.Column(sql.Integer, sql.ForeignKey("user.id", ondelete="CASCADE"))

rating = sql.Column(sql.Integer, nullable=False)
comment = sql.Column(sql.String(length=description_length), nullable=True)

surface = sql.Column(sql.String(length=name_length), nullable=False)
surface_metadata = sql.Column(sql.JSON, default={}, nullable=False)
21 changes: 18 additions & 3 deletions querybook/webapp/components/AIAssistant/QueryGenerationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { QueryComparison } from 'components/TranspileQueryModal/QueryComparison'
import { AICommandType } from 'const/aiAssistant';
import { ComponentType, ElementType } from 'const/analytics';
import { IQueryEngine } from 'const/queryEngine';
import { SurveySurfaceType } from 'const/survey';
import { useSurveyTrigger } from 'hooks/ui/useSurveyTrigger';
import { useAISocket } from 'hooks/useAISocket';
import { trackClick } from 'lib/analytics';
import { TableToken } from 'lib/sql-helper/sql-lexer';
Expand Down Expand Up @@ -115,7 +117,7 @@ export const QueryGenerationModal = ({

useEffect(() => {
if (!generating) {
setTables(uniq([...tablesInQuery, ...tables]));
setTables((tables) => uniq([...tablesInQuery, ...tables]));
}
}, [tablesInQuery, generating]);

Expand All @@ -125,12 +127,25 @@ export const QueryGenerationModal = ({
setNewQuery(trimSQLQuery(rawNewQuery));
}, [rawNewQuery]);

const triggerSurvey = useSurveyTrigger();
useEffect(() => {
if (!newQuery || generating) {
return;
}
triggerSurvey(SurveySurfaceType.TEXT_TO_SQL, {
question,
tables,
query: newQuery,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newQuery, triggerSurvey, generating]);

const onGenerate = useCallback(() => {
setFoundTables([]);
generateSQL({
query_engine_id: engineId,
tables: tables,
question: question,
tables,
question,
original_query: query,
});
trackClick({
Expand Down
16 changes: 13 additions & 3 deletions querybook/webapp/components/DataDocQueryCell/DataDocQueryCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { UDFForm } from 'components/UDFForm/UDFForm';
import { ComponentType, ElementType } from 'const/analytics';
import { IDataQueryCellMeta, TDataDocMetaVariables } from 'const/datadoc';
import type { IQueryEngine, IQueryTranspiler } from 'const/queryEngine';
import { SurveySurfaceType } from 'const/survey';
import { triggerSurvey } from 'hooks/ui/useSurveyTrigger';
import { trackClick } from 'lib/analytics';
import CodeMirror from 'lib/codemirror';
import { createSQLLinter } from 'lib/codemirror/codemirror-lint';
Expand Down Expand Up @@ -418,14 +420,22 @@ class DataDocQueryCellComponent extends React.PureComponent<IProps, IState> {
return runQuery(
await this.getTransformedQuery(),
this.engineId,
async (query, engineId) =>
(
async (query, engineId) => {
const queryId = (
await this.props.createQueryExecution(
query,
engineId,
this.props.cellId
)
).id
).id;

triggerSurvey(SurveySurfaceType.QUERY_AUTHORING, {
query_execution_id: queryId,
cell_id: this.props.cellId,
});

return queryId;
}
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { OptionTypeBase } from 'react-select';

export interface IDataDocBoardsSelectProps {
onChange: (params: OptionTypeBase[]) => void;
value: IOption<number>[];
value: Array<IOption<number>>;
label?: string;
name: string;
}
Expand All @@ -23,14 +23,16 @@ export const DataDocBoardsSelect: React.FC<IDataDocBoardsSelectProps> = ({
(state: IStoreState) => state.board.boardById
);

const boardOptions: IOption<number>[] = useMemo(() => {
return Object.values(boardById).map((board) => ({
value: board.id,
label: board.name,
}));
}, [boardById]);
const boardOptions: Array<IOption<number>> = useMemo(
() =>
Object.values(boardById).map((board) => ({
value: board.id,
label: board.name,
})),
[boardById]
);

const selectedBoards: IOption<number>[] = useMemo(
const selectedBoards: Array<IOption<number>> = useMemo(
() =>
boardOptions.filter((board) =>
value.map((v) => v.value).includes(board.value)
Expand Down
Loading
Loading