chore: improve analytics (#11714)
* chore: improve analytics * lint * log more events, add note in UPDATING.md * handling base class * more events\! * get ref through * right before @expose * fix context * touchups
This commit is contained in:
parent
9215a31fa2
commit
0504cf1a00
|
|
@ -25,7 +25,10 @@ assists people when migrating to a new version.
|
|||
## Next
|
||||
|
||||
- [11704](https://github.com/apache/incubator-superset/pull/11704) Breaking change: Jinja templating for SQL queries has been updated, removing default modules such as `datetime` and `random` and enforcing static template values. To restore or extend functionality, use `JINJA_CONTEXT_ADDONS` and `CUSTOM_TEMPLATE_PROCESSORS` in `superset_config.py`.
|
||||
|
||||
- [11714](https://github.com/apache/incubator-superset/pull/11714): Logs
|
||||
significantly more analytics events (roughly double?), and when
|
||||
using DBEventLogger (default) could result in stressing the metadata
|
||||
database more.
|
||||
- [11509](https://github.com/apache/incubator-superset/pull/11509): Config value `TABLE_NAMES_CACHE_CONFIG` has been renamed to `DATA_CACHE_CONFIG`, which will now also hold query results cache from connected datasources (previously held in `CACHE_CONFIG`), in addition to the table names. If you will set `DATA_CACHE_CONFIG` to a new cache backend different than your previous `CACHE_CONFIG`, plan for additional cache warmup to avoid degrading charting performance for the end users.
|
||||
|
||||
- [11575](https://github.com/apache/incubator-superset/pull/11575) The Row Level Security (RLS) config flag has been moved to a feature flag. To migrate, add `ROW_LEVEL_SECURITY: True` to the `FEATURE_FLAGS` dict in `superset_config.py`.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from superset.annotation_layers.schemas import (
|
|||
openapi_spec_methods_override,
|
||||
)
|
||||
from superset.constants import RouteMethod
|
||||
from superset.extensions import event_logger
|
||||
from superset.models.annotations import AnnotationLayer
|
||||
from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
|
||||
|
||||
|
|
@ -110,6 +111,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@permission_name("delete")
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def delete(self, pk: int) -> Response:
|
||||
"""Delete an annotation layer
|
||||
---
|
||||
|
|
@ -159,6 +161,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@permission_name("post")
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def post(self) -> Response:
|
||||
"""Creates a new Annotation Layer
|
||||
---
|
||||
|
|
@ -218,6 +221,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@permission_name("put")
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def put(self, pk: int) -> Response:
|
||||
"""Updates an Annotation Layer
|
||||
---
|
||||
|
|
@ -284,6 +288,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_delete_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def bulk_delete(self, **kwargs: Any) -> Response:
|
||||
"""Delete bulk Annotation layers
|
||||
---
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ class CacheRestApi(BaseSupersetModelRestApi):
|
|||
openapi_spec_component_schemas = (CacheInvalidationRequestSchema,)
|
||||
|
||||
@expose("/invalidate", methods=["POST"])
|
||||
@event_logger.log_this
|
||||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def invalidate(self) -> Response:
|
||||
"""
|
||||
Takes a list of datasources, finds the associated cache records and
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def post(self) -> Response:
|
||||
"""Creates a new Chart
|
||||
---
|
||||
|
|
@ -270,6 +271,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def put(self, pk: int) -> Response:
|
||||
"""Changes a Chart
|
||||
---
|
||||
|
|
@ -343,6 +345,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def delete(self, pk: int) -> Response:
|
||||
"""Deletes a Chart
|
||||
---
|
||||
|
|
@ -393,6 +396,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_delete_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def bulk_delete(self, **kwargs: Any) -> Response:
|
||||
"""Delete bulk Charts
|
||||
---
|
||||
|
|
@ -444,10 +448,10 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
return self.response_422(message=str(ex))
|
||||
|
||||
@expose("/data", methods=["POST"])
|
||||
@event_logger.log_this
|
||||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def data(self) -> Response:
|
||||
"""
|
||||
Takes a query context constructed in the client and returns payload
|
||||
|
|
@ -532,6 +536,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
@rison(screenshot_query_schema)
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def cache_screenshot(self, pk: int, **kwargs: Dict[str, bool]) -> WerkzeugResponse:
|
||||
"""
|
||||
---
|
||||
|
|
@ -604,6 +609,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
@rison(screenshot_query_schema)
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def screenshot(self, pk: int, digest: str) -> WerkzeugResponse:
|
||||
"""Get Chart screenshot
|
||||
---
|
||||
|
|
@ -657,6 +663,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
@rison(thumbnail_query_schema)
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def thumbnail(
|
||||
self, pk: int, digest: str, **kwargs: Dict[str, bool]
|
||||
) -> WerkzeugResponse:
|
||||
|
|
@ -730,6 +737,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_export_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def export(self, **kwargs: Any) -> Response:
|
||||
"""Export charts
|
||||
---
|
||||
|
|
@ -787,6 +795,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_fav_star_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def favorite_status(self, **kwargs: Any) -> Response:
|
||||
"""Favorite stars for Charts
|
||||
---
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from superset.css_templates.schemas import (
|
|||
get_delete_ids_schema,
|
||||
openapi_spec_methods_override,
|
||||
)
|
||||
from superset.extensions import event_logger
|
||||
from superset.models.core import CssTemplate
|
||||
from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
|
||||
|
||||
|
|
@ -87,6 +88,7 @@ class CssTemplateRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_delete_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def bulk_delete(self, **kwargs: Any) -> Response:
|
||||
"""Delete bulk CSS Templates
|
||||
---
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ from superset.dashboards.schemas import (
|
|||
openapi_spec_methods_override,
|
||||
thumbnail_query_schema,
|
||||
)
|
||||
from superset.extensions import event_logger
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.tasks.thumbnails import cache_dashboard_thumbnail
|
||||
from superset.utils.screenshots import DashboardScreenshot
|
||||
|
|
@ -209,6 +210,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def post(self) -> Response:
|
||||
"""Creates a new Dashboard
|
||||
---
|
||||
|
|
@ -267,6 +269,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def put(self, pk: int) -> Response:
|
||||
"""Changes a Dashboard
|
||||
---
|
||||
|
|
@ -337,6 +340,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def delete(self, pk: int) -> Response:
|
||||
"""Deletes a Dashboard
|
||||
---
|
||||
|
|
@ -387,6 +391,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_delete_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def bulk_delete(self, **kwargs: Any) -> Response:
|
||||
"""Delete bulk Dashboards
|
||||
---
|
||||
|
|
@ -444,6 +449,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_export_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def export(self, **kwargs: Any) -> Response:
|
||||
"""Export dashboards
|
||||
---
|
||||
|
|
@ -519,6 +525,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@rison(thumbnail_query_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def thumbnail(
|
||||
self, pk: int, digest: str, **kwargs: Dict[str, bool]
|
||||
) -> WerkzeugResponse:
|
||||
|
|
@ -606,6 +613,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_fav_star_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def favorite_status(self, **kwargs: Any) -> Response:
|
||||
"""Favorite Stars for Dashboards
|
||||
---
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def post(self) -> Response:
|
||||
"""Creates a new Database
|
||||
---
|
||||
|
|
@ -247,6 +248,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def put( # pylint: disable=too-many-return-statements, arguments-differ
|
||||
self, pk: int
|
||||
) -> Response:
|
||||
|
|
@ -320,6 +322,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def delete(self, pk: int) -> Response: # pylint: disable=arguments-differ
|
||||
"""Deletes a Database
|
||||
---
|
||||
|
|
@ -370,6 +373,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@rison(database_schemas_query_schema)
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def schemas(self, pk: int, **kwargs: Any) -> FlaskResponse:
|
||||
"""Get all schemas from a database
|
||||
---
|
||||
|
|
@ -423,8 +427,8 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@check_datasource_access
|
||||
@safe
|
||||
@event_logger.log_this
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def table_metadata(
|
||||
self, database: Database, table_name: str, schema_name: str
|
||||
) -> FlaskResponse:
|
||||
|
|
@ -480,8 +484,8 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@check_datasource_access
|
||||
@safe
|
||||
@event_logger.log_this
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def select_star(
|
||||
self, database: Database, table_name: str, schema_name: Optional[str] = None
|
||||
) -> FlaskResponse:
|
||||
|
|
@ -537,8 +541,8 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||
@expose("/test_connection", methods=["POST"])
|
||||
@protect()
|
||||
@safe
|
||||
@event_logger.log_this
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def test_connection( # pylint: disable=too-many-return-statements
|
||||
self,
|
||||
) -> FlaskResponse:
|
||||
|
|
@ -618,6 +622,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def related_objects(self, pk: int) -> Response:
|
||||
"""Get charts and dashboards count associated to a database
|
||||
---
|
||||
|
|
@ -676,6 +681,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_export_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def export(self, **kwargs: Any) -> Response:
|
||||
"""Export database(s) with associated datasets
|
||||
---
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
|
|||
from flask_babel import ngettext
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from superset import is_feature_enabled
|
||||
from superset import event_logger, is_feature_enabled
|
||||
from superset.commands.exceptions import CommandInvalidError
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.constants import RouteMethod
|
||||
|
|
@ -182,6 +182,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def post(self) -> Response:
|
||||
"""Creates a new Dataset
|
||||
---
|
||||
|
|
@ -238,6 +239,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def put(self, pk: int) -> Response:
|
||||
"""Changes a Dataset
|
||||
---
|
||||
|
|
@ -308,6 +310,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def delete(self, pk: int) -> Response:
|
||||
"""Deletes a Dataset
|
||||
---
|
||||
|
|
@ -358,6 +361,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_export_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def export(self, **kwargs: Any) -> Response:
|
||||
"""Export datasets
|
||||
---
|
||||
|
|
@ -433,6 +437,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def refresh(self, pk: int) -> Response:
|
||||
"""Refresh a Dataset
|
||||
---
|
||||
|
|
@ -482,6 +487,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def related_objects(self, pk: int) -> Response:
|
||||
"""Get charts and dashboards count associated to a dataset
|
||||
---
|
||||
|
|
@ -540,6 +546,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
@safe
|
||||
@statsd_metrics
|
||||
@rison(get_delete_ids_schema)
|
||||
@event_logger.log_this_with_context(log_to_statsd=False)
|
||||
def bulk_delete(self, **kwargs: Any) -> Response:
|
||||
"""Delete bulk Datasets
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Add path to logs
|
||||
|
||||
Revision ID: a8173232b786
|
||||
Revises: 49b5a32daba5
|
||||
Create Date: 2020-11-15 16:08:24.580764
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a8173232b786"
|
||||
down_revision = "49b5a32daba5"
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column("logs", sa.Column("path", sa.String(length=256), nullable=True))
|
||||
op.add_column(
|
||||
"logs", sa.Column("path_no_int", sa.String(length=256), nullable=True)
|
||||
)
|
||||
op.add_column("logs", sa.Column("ref", sa.String(length=256), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column("logs", "path")
|
||||
op.drop_column("logs", "path_no_int")
|
||||
op.drop_column("logs", "ref")
|
||||
|
|
@ -715,6 +715,9 @@ class Log(Model): # pylint: disable=too-few-public-methods
|
|||
dttm = Column(DateTime, default=datetime.utcnow)
|
||||
duration_ms = Column(Integer)
|
||||
referrer = Column(String(1024))
|
||||
path = Column(String(256))
|
||||
path_no_int = Column(String(256))
|
||||
ref = Column(String(256))
|
||||
|
||||
|
||||
class FavStarClassName(str, Enum):
|
||||
|
|
|
|||
|
|
@ -30,15 +30,35 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||
from superset.stats_logger import BaseStatsLogger
|
||||
|
||||
|
||||
def strip_int_from_path(path: Optional[str]) -> str:
|
||||
"""Simple function to remove ints from '/' separated paths"""
|
||||
if path:
|
||||
return "/".join(["<int>" if s.isdigit() else s for s in path.split("/")])
|
||||
return ""
|
||||
|
||||
|
||||
class AbstractEventLogger(ABC):
|
||||
@abstractmethod
|
||||
def log(
|
||||
self, user_id: Optional[int], action: str, *args: Any, **kwargs: Any
|
||||
def log( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
user_id: Optional[int],
|
||||
action: str,
|
||||
dashboard_id: Optional[int],
|
||||
duration_ms: Optional[int],
|
||||
slice_id: Optional[int],
|
||||
path: Optional[str],
|
||||
path_no_int: Optional[str],
|
||||
ref: Optional[str],
|
||||
referrer: Optional[str],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@contextmanager
|
||||
def log_context(self, action: str) -> Iterator[Callable[..., None]]:
|
||||
def log_context(
|
||||
self, action: str, ref: Optional[str] = None, log_to_statsd: bool = True,
|
||||
) -> Iterator[Callable[..., None]]:
|
||||
"""
|
||||
Log an event while reading information from the request context.
|
||||
`kwargs` will be appended directly to the log payload.
|
||||
|
|
@ -69,7 +89,8 @@ class AbstractEventLogger(ABC):
|
|||
except (TypeError, ValueError):
|
||||
slice_id = 0
|
||||
|
||||
self.stats_logger.incr(action)
|
||||
if log_to_statsd:
|
||||
self.stats_logger.incr(action)
|
||||
|
||||
# bulk insert
|
||||
try:
|
||||
|
|
@ -86,18 +107,38 @@ class AbstractEventLogger(ABC):
|
|||
slice_id=slice_id,
|
||||
duration_ms=round((time.time() - start_time) * 1000),
|
||||
referrer=referrer,
|
||||
path=request.path,
|
||||
path_no_int=strip_int_from_path(request.path),
|
||||
ref=ref,
|
||||
)
|
||||
|
||||
def log_this(self, f: Callable[..., Any]) -> Callable[..., Any]:
|
||||
def _wrapper(
|
||||
self, f: Callable[..., Any], **wrapper_kwargs: Any
|
||||
) -> Callable[..., Any]:
|
||||
action_str = wrapper_kwargs.get("action") or f.__name__
|
||||
ref = f.__qualname__ if hasattr(f, "__qualname__") else None
|
||||
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
with self.log_context(f.__name__) as log:
|
||||
with self.log_context(action_str, ref, **wrapper_kwargs) as log:
|
||||
value = f(*args, **kwargs)
|
||||
log(**kwargs)
|
||||
return value
|
||||
|
||||
return wrapper
|
||||
|
||||
def log_this(self, f: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Decorator that uses the function name as the action"""
|
||||
return self._wrapper(f)
|
||||
|
||||
def log_this_with_context(self, **kwargs: Any) -> Callable[..., Any]:
|
||||
"""Decorator that can override kwargs of log_context"""
|
||||
|
||||
def func(f: Callable[..., Any]) -> Callable[..., Any]:
|
||||
return self._wrapper(f, **kwargs)
|
||||
|
||||
return func
|
||||
|
||||
def log_manually(self, f: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Allow a function to manually update"""
|
||||
|
||||
|
|
@ -162,16 +203,23 @@ def get_event_logger_from_cfg_value(cfg_value: Any) -> AbstractEventLogger:
|
|||
class DBEventLogger(AbstractEventLogger):
|
||||
"""Event logger that commits logs to Superset DB"""
|
||||
|
||||
def log( # pylint: disable=too-many-locals
|
||||
self, user_id: Optional[int], action: str, *args: Any, **kwargs: Any
|
||||
def log( # pylint: disable=too-many-arguments,too-many-locals
|
||||
self,
|
||||
user_id: Optional[int],
|
||||
action: str,
|
||||
dashboard_id: Optional[int],
|
||||
duration_ms: Optional[int],
|
||||
slice_id: Optional[int],
|
||||
path: Optional[str],
|
||||
path_no_int: Optional[str],
|
||||
ref: Optional[str],
|
||||
referrer: Optional[str],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
from superset.models.core import Log
|
||||
|
||||
records = kwargs.get("records", list())
|
||||
dashboard_id = kwargs.get("dashboard_id")
|
||||
slice_id = kwargs.get("slice_id")
|
||||
duration_ms = kwargs.get("duration_ms")
|
||||
referrer = kwargs.get("referrer")
|
||||
|
||||
logs = list()
|
||||
for record in records:
|
||||
|
|
@ -188,6 +236,9 @@ class DBEventLogger(AbstractEventLogger):
|
|||
duration_ms=duration_ms,
|
||||
referrer=referrer,
|
||||
user_id=user_id,
|
||||
path=path,
|
||||
path_no_int=path_no_int,
|
||||
ref=ref,
|
||||
)
|
||||
logs.append(log)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from marshmallow import fields, Schema
|
|||
from sqlalchemy import and_, distinct, func
|
||||
from sqlalchemy.orm.query import Query
|
||||
|
||||
from superset.extensions import db, security_manager
|
||||
from superset.extensions import db, event_logger, security_manager
|
||||
from superset.models.core import FavStar
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.slice import Slice
|
||||
|
|
@ -49,6 +49,7 @@ get_related_schema = {
|
|||
"filter": {"type": "string"},
|
||||
},
|
||||
}
|
||||
log_context = event_logger.log_context
|
||||
|
||||
|
||||
class RelatedResultResponseSchema(Schema):
|
||||
|
|
@ -312,49 +313,61 @@ class BaseSupersetModelRestApi(ModelRestApi):
|
|||
"""
|
||||
Add statsd metrics to builtin FAB _info endpoint
|
||||
"""
|
||||
duration, response = time_function(super().info_headless, **kwargs)
|
||||
self.send_stats_metrics(response, self.info.__name__, duration)
|
||||
return response
|
||||
ref = f"{self.__class__.__name__}.info"
|
||||
with log_context(ref, ref, log_to_statsd=False):
|
||||
duration, response = time_function(super().info_headless, **kwargs)
|
||||
self.send_stats_metrics(response, self.info.__name__, duration)
|
||||
return response
|
||||
|
||||
def get_headless(self, pk: int, **kwargs: Any) -> Response:
|
||||
"""
|
||||
Add statsd metrics to builtin FAB GET endpoint
|
||||
"""
|
||||
duration, response = time_function(super().get_headless, pk, **kwargs)
|
||||
self.send_stats_metrics(response, self.get.__name__, duration)
|
||||
return response
|
||||
ref = f"{self.__class__.__name__}.get"
|
||||
with log_context(ref, ref, log_to_statsd=False):
|
||||
duration, response = time_function(super().get_headless, pk, **kwargs)
|
||||
self.send_stats_metrics(response, self.get.__name__, duration)
|
||||
return response
|
||||
|
||||
def get_list_headless(self, **kwargs: Any) -> Response:
|
||||
"""
|
||||
Add statsd metrics to builtin FAB GET list endpoint
|
||||
"""
|
||||
duration, response = time_function(super().get_list_headless, **kwargs)
|
||||
self.send_stats_metrics(response, self.get_list.__name__, duration)
|
||||
return response
|
||||
ref = f"{self.__class__.__name__}.get_list"
|
||||
with log_context(ref, ref, log_to_statsd=False):
|
||||
duration, response = time_function(super().get_list_headless, **kwargs)
|
||||
self.send_stats_metrics(response, self.get_list.__name__, duration)
|
||||
return response
|
||||
|
||||
def post_headless(self) -> Response:
|
||||
"""
|
||||
Add statsd metrics to builtin FAB POST endpoint
|
||||
"""
|
||||
duration, response = time_function(super().post_headless)
|
||||
self.send_stats_metrics(response, self.post.__name__, duration)
|
||||
return response
|
||||
ref = f"{self.__class__.__name__}.post"
|
||||
with log_context(ref, ref, log_to_statsd=False):
|
||||
duration, response = time_function(super().post_headless)
|
||||
self.send_stats_metrics(response, self.post.__name__, duration)
|
||||
return response
|
||||
|
||||
def put_headless(self, pk: int) -> Response:
|
||||
"""
|
||||
Add statsd metrics to builtin FAB PUT endpoint
|
||||
"""
|
||||
duration, response = time_function(super().put_headless, pk)
|
||||
self.send_stats_metrics(response, self.put.__name__, duration)
|
||||
return response
|
||||
ref = f"{self.__class__.__name__}.put"
|
||||
with log_context(ref, ref, log_to_statsd=False):
|
||||
duration, response = time_function(super().put_headless, pk)
|
||||
self.send_stats_metrics(response, self.put.__name__, duration)
|
||||
return response
|
||||
|
||||
def delete_headless(self, pk: int) -> Response:
|
||||
"""
|
||||
Add statsd metrics to builtin FAB DELETE endpoint
|
||||
"""
|
||||
duration, response = time_function(super().delete_headless, pk)
|
||||
self.send_stats_metrics(response, self.delete.__name__, duration)
|
||||
return response
|
||||
ref = f"{self.__class__.__name__}.delete"
|
||||
with log_context(ref, ref, log_to_statsd=False):
|
||||
duration, response = time_function(super().delete_headless, pk)
|
||||
self.send_stats_metrics(response, self.delete.__name__, duration)
|
||||
return response
|
||||
|
||||
@expose("/related/<column_name>", methods=["GET"])
|
||||
@protect()
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/datasources/")
|
||||
def datasources(self) -> FlaskResponse:
|
||||
return self.json_response(
|
||||
|
|
@ -169,6 +170,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
)
|
||||
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/override_role_permissions/", methods=["POST"])
|
||||
def override_role_permissions(self) -> FlaskResponse:
|
||||
"""Updates the role with the give datasource permissions.
|
||||
|
|
@ -220,8 +222,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
{"granted": granted_perms, "requested": list(db_ds_names)}, status=201
|
||||
)
|
||||
|
||||
@event_logger.log_this
|
||||
@has_access
|
||||
@event_logger.log_this
|
||||
@expose("/request_access/")
|
||||
def request_access(self) -> FlaskResponse:
|
||||
datasources = set()
|
||||
|
|
@ -263,8 +265,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
datasource_names=", ".join([o.name for o in datasources]),
|
||||
)
|
||||
|
||||
@event_logger.log_this
|
||||
@has_access
|
||||
@event_logger.log_this
|
||||
@expose("/approve")
|
||||
def approve(self) -> FlaskResponse: # pylint: disable=too-many-locals,no-self-use
|
||||
def clean_fulfilled_requests(session: Session) -> None:
|
||||
|
|
@ -368,6 +370,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
return redirect("/accessrequestsmodelview/list/")
|
||||
|
||||
@has_access
|
||||
@event_logger.log_this
|
||||
@expose("/slice/<int:slice_id>/")
|
||||
def slice(self, slice_id: int) -> FlaskResponse: # pylint: disable=no-self-use
|
||||
_, slc = get_form_data(slice_id, use_slice_data=True)
|
||||
|
|
@ -450,9 +453,9 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
except SupersetException as ex:
|
||||
return json_error_response(utils.error_msg_from_exception(ex))
|
||||
|
||||
@event_logger.log_this
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/annotation_json/<int:layer_id>")
|
||||
def annotation_json( # pylint: disable=no-self-use
|
||||
self, layer_id: int
|
||||
|
|
@ -484,10 +487,10 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
if not is_feature_enabled("ENABLE_EXPLORE_JSON_CSRF_PROTECTION"):
|
||||
EXPLORE_JSON_METHODS.append("GET")
|
||||
|
||||
@event_logger.log_this
|
||||
@api
|
||||
@has_access_api
|
||||
@handle_api_exception
|
||||
@event_logger.log_this
|
||||
@expose(
|
||||
"/explore_json/<datasource_type>/<int:datasource_id>/",
|
||||
methods=EXPLORE_JSON_METHODS,
|
||||
|
|
@ -535,8 +538,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
except SupersetException as ex:
|
||||
return json_error_response(utils.error_msg_from_exception(ex), 400)
|
||||
|
||||
@event_logger.log_this
|
||||
@has_access
|
||||
@event_logger.log_this
|
||||
@expose("/import_dashboards", methods=["GET", "POST"])
|
||||
def import_dashboards(self) -> FlaskResponse:
|
||||
"""Overrides the dashboards using json instances from the file."""
|
||||
|
|
@ -578,8 +581,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
"superset/import_dashboards.html", databases=databases
|
||||
)
|
||||
|
||||
@event_logger.log_this
|
||||
@has_access
|
||||
@event_logger.log_this
|
||||
@expose("/explore/<datasource_type>/<int:datasource_id>/", methods=["GET", "POST"])
|
||||
@expose("/explore/", methods=["GET", "POST"])
|
||||
def explore( # pylint: disable=too-many-locals,too-many-return-statements
|
||||
|
|
@ -733,6 +736,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
@api
|
||||
@handle_api_exception
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/filter/<datasource_type>/<int:datasource_id>/<column>/")
|
||||
def filter( # pylint: disable=no-self-use
|
||||
self, datasource_type: str, datasource_id: int, column: str
|
||||
|
|
@ -881,6 +885,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/schemas/<int:db_id>/")
|
||||
@expose("/schemas/<int:db_id>/<force_refresh>/")
|
||||
def schemas( # pylint: disable=no-self-use
|
||||
|
|
@ -905,6 +910,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/tables/<int:db_id>/<schema>/<substr>/")
|
||||
@expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/")
|
||||
def tables( # pylint: disable=too-many-locals,no-self-use
|
||||
|
|
@ -1014,6 +1020,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/copy_dash/<int:dashboard_id>/", methods=["GET", "POST"])
|
||||
def copy_dash( # pylint: disable=no-self-use
|
||||
self, dashboard_id: int
|
||||
|
|
@ -1063,6 +1070,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/save_dash/<int:dashboard_id>/", methods=["GET", "POST"])
|
||||
def save_dash( # pylint: disable=no-self-use
|
||||
self, dashboard_id: int
|
||||
|
|
@ -1101,6 +1109,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/add_slices/<int:dashboard_id>/", methods=["POST"])
|
||||
def add_slices( # pylint: disable=no-self-use
|
||||
self, dashboard_id: int
|
||||
|
|
@ -1119,6 +1128,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/testconn", methods=["POST", "GET"])
|
||||
def testconn( # pylint: disable=too-many-return-statements,no-self-use
|
||||
self,
|
||||
|
|
@ -1199,6 +1209,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/recent_activity/<int:user_id>/", methods=["GET"])
|
||||
def recent_activity( # pylint: disable=no-self-use
|
||||
self, user_id: int
|
||||
|
|
@ -1297,6 +1308,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/csrf_token/", methods=["GET"])
|
||||
def csrf_token(self) -> FlaskResponse:
|
||||
return Response(
|
||||
|
|
@ -1305,6 +1317,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/available_domains/", methods=["GET"])
|
||||
def available_domains(self) -> FlaskResponse: # pylint: disable=no-self-use
|
||||
"""
|
||||
|
|
@ -1318,6 +1331,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/fave_dashboards_by_username/<username>/", methods=["GET"])
|
||||
def fave_dashboards_by_username(self, username: str) -> FlaskResponse:
|
||||
"""This lets us use a user's username to pull favourite dashboards"""
|
||||
|
|
@ -1326,6 +1340,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/fave_dashboards/<int:user_id>/", methods=["GET"])
|
||||
def fave_dashboards( # pylint: disable=no-self-use
|
||||
self, user_id: int
|
||||
|
|
@ -1360,6 +1375,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/created_dashboards/<int:user_id>/", methods=["GET"])
|
||||
def created_dashboards( # pylint: disable=no-self-use
|
||||
self, user_id: int
|
||||
|
|
@ -1388,6 +1404,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/user_slices", methods=["GET"])
|
||||
@expose("/user_slices/<int:user_id>/", methods=["GET"])
|
||||
def user_slices( # pylint: disable=no-self-use
|
||||
|
|
@ -1439,6 +1456,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/created_slices", methods=["GET"])
|
||||
@expose("/created_slices/<int:user_id>/", methods=["GET"])
|
||||
def created_slices( # pylint: disable=no-self-use
|
||||
|
|
@ -1466,6 +1484,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/fave_slices", methods=["GET"])
|
||||
@expose("/fave_slices/<int:user_id>/", methods=["GET"])
|
||||
def fave_slices( # pylint: disable=no-self-use
|
||||
|
|
@ -1596,6 +1615,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
return json_success(json.dumps(result))
|
||||
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/favstar/<class_name>/<int:obj_id>/<action>/")
|
||||
def favstar( # pylint: disable=no-self-use
|
||||
self, class_name: str, obj_id: int, action: str
|
||||
|
|
@ -1629,6 +1649,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/dashboard/<int:dashboard_id>/published/", methods=("GET", "POST"))
|
||||
def publish( # pylint: disable=no-self-use
|
||||
self, dashboard_id: int
|
||||
|
|
@ -1775,7 +1796,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
)
|
||||
|
||||
@api
|
||||
@event_logger.log_this
|
||||
@has_access
|
||||
@expose("/log/", methods=["POST"])
|
||||
def log(self) -> FlaskResponse: # pylint: disable=no-self-use
|
||||
|
|
@ -2118,8 +2138,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
return self.json_response("OK")
|
||||
|
||||
@has_access_api
|
||||
@expose("/validate_sql_json/", methods=["POST", "GET"])
|
||||
@event_logger.log_this
|
||||
@expose("/validate_sql_json/", methods=["POST", "GET"])
|
||||
def validate_sql_json( # pylint: disable=too-many-locals,too-many-return-statements,no-self-use
|
||||
self,
|
||||
) -> FlaskResponse:
|
||||
|
|
@ -2304,8 +2324,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@has_access_api
|
||||
@handle_api_exception
|
||||
@expose("/sql_json/", methods=["POST"])
|
||||
@event_logger.log_this
|
||||
@expose("/sql_json/", methods=["POST"])
|
||||
def sql_json(self) -> FlaskResponse:
|
||||
log_params = {
|
||||
"user_agent": cast(Optional[str], request.headers.get("USER_AGENT"))
|
||||
|
|
@ -2438,8 +2458,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
)
|
||||
|
||||
@has_access
|
||||
@expose("/csv/<client_id>")
|
||||
@event_logger.log_this
|
||||
@expose("/csv/<client_id>")
|
||||
def csv(self, client_id: str) -> FlaskResponse: # pylint: disable=no-self-use
|
||||
"""Download the query results as csv."""
|
||||
logger.info("Exporting CSV file [%s]", client_id)
|
||||
|
|
@ -2495,8 +2515,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
@api
|
||||
@handle_api_exception
|
||||
@has_access
|
||||
@expose("/fetch_datasource_metadata")
|
||||
@event_logger.log_this
|
||||
@expose("/fetch_datasource_metadata")
|
||||
def fetch_datasource_metadata(self) -> FlaskResponse: # pylint: disable=no-self-use
|
||||
"""
|
||||
Fetch the datasource metadata.
|
||||
|
|
@ -2517,6 +2537,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
return json_success(json.dumps(datasource.data))
|
||||
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/queries/<float:last_updated_ms>")
|
||||
@expose("/queries/<int:last_updated_ms>")
|
||||
def queries(self, last_updated_ms: Union[float, int]) -> FlaskResponse:
|
||||
|
|
@ -2550,8 +2571,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
return json_success(json.dumps(dict_queries, default=utils.json_int_dttm_ser))
|
||||
|
||||
@has_access
|
||||
@expose("/search_queries")
|
||||
@event_logger.log_this
|
||||
@expose("/search_queries")
|
||||
def search_queries(self) -> FlaskResponse: # pylint: disable=no-self-use
|
||||
"""
|
||||
Search for previously run sqllab queries. Used for Sqllab Query Search
|
||||
|
|
@ -2621,6 +2642,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
500,
|
||||
)
|
||||
|
||||
@event_logger.log_this
|
||||
@expose("/welcome")
|
||||
def welcome(self) -> FlaskResponse:
|
||||
"""Personalized welcome page"""
|
||||
|
|
@ -2651,6 +2673,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
)
|
||||
|
||||
@has_access
|
||||
@event_logger.log_this
|
||||
@expose("/profile/<username>/")
|
||||
def profile(self, username: str) -> FlaskResponse:
|
||||
"""User profile page"""
|
||||
|
|
@ -2721,6 +2744,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
}
|
||||
|
||||
@has_access
|
||||
@event_logger.log_this
|
||||
@expose("/sqllab", methods=["GET", "POST"])
|
||||
def sqllab(self) -> FlaskResponse:
|
||||
"""SQL Editor"""
|
||||
|
|
@ -2747,7 +2771,9 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
)
|
||||
|
||||
@has_access
|
||||
@event_logger.log_this
|
||||
@expose("/sqllab/history/", methods=["GET"])
|
||||
@event_logger.log_this
|
||||
def sqllab_search(self) -> FlaskResponse:
|
||||
if not (
|
||||
is_feature_enabled("ENABLE_REACT_CRUD_VIEWS")
|
||||
|
|
@ -2759,6 +2785,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
|
||||
@api
|
||||
@has_access_api
|
||||
@event_logger.log_this
|
||||
@expose("/schemas_access_for_csv_upload")
|
||||
def schemas_access_for_csv_upload(self) -> FlaskResponse:
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in New Issue