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 2 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
10 changes: 10 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,12 @@ 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

# Please refer to the querybook guide on how to enable surveys
surfaces: []
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)) {
czgu marked this conversation as resolved.
Show resolved Hide resolved
return;
}
triggerSurvey(SurveySurfaceType.QUERY_AUTHORING, {
czgu marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -9,7 +9,9 @@ import {
tableNameDataTransferName,
tableNameDraggableType,
} from 'const/metastore';
import { SurveySurfaceType } from 'const/survey';
import { useShallowSelector } from 'hooks/redux/useShallowSelector';
import { useSurveyTrigger } from 'hooks/ui/useSurveyTrigger';
import { queryMetastoresSelector } from 'redux/dataSources/selector';
import * as dataTableSearchActions from 'redux/dataTableSearch/action';
import {
Expand Down Expand Up @@ -140,6 +142,7 @@ export const DataTableNavigator: React.FC<IDataTableNavigatorProps> = ({
if (noMetastore) {
resetSearch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [noMetastore]);

const queryMetastore = useMemo(
Expand Down Expand Up @@ -185,6 +188,19 @@ export const DataTableNavigator: React.FC<IDataTableNavigatorProps> = ({
[updateSearchString]
);

const triggerSurvey = useSurveyTrigger();
useEffect(() => {
if (searchString === '') {
return;
}
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved

triggerSurvey(SurveySurfaceType.TABLE_SEARCH, {
search_query: searchString,
search_filter: Object.keys(searchFilters),
is_modal: false,
});
}, [searchString, searchFilters, triggerSurvey]);

const tableRowRenderer = useCallback(
(table: ITableResultWithSelection) => (
<TableRow
Expand Down
13 changes: 13 additions & 0 deletions querybook/webapp/components/DataTableView/DataTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
MetadataMode,
MetadataType,
} from 'const/metastore';
import { SurveySurfaceType } from 'const/survey';
import { useShallowSelector } from 'hooks/redux/useShallowSelector';
import { useSurveyTrigger } from 'hooks/ui/useSurveyTrigger';
import { useTrackView } from 'hooks/useTrackView';
import { trackClick } from 'lib/analytics';
import { setBrowserTitle } from 'lib/querybookUI';
Expand Down Expand Up @@ -176,6 +178,17 @@ export const DataTableView: React.FC<IDataTableViewProps> = ({ tableId }) => {
});

useTrackView(ComponentType.TABLE_DETAIL_VIEW);
const triggerSurvey = useSurveyTrigger(true);
useEffect(() => {
if (!tableId || !tableName) {
return;
}
triggerSurvey(SurveySurfaceType.TABLE_TRUST, {
table_id: tableId,
table_name: tableName,
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved
});
}, [tableId, tableName, triggerSurvey]);

useEffect(() => {
getTable(tableId);
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
Loading
Loading